@thedesignagent/mcp 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { findChromeExecutable, isLoginUrl } from './screenshot.js';
3
+ import { saveStoredAuth } from './auth-store.js';
4
+ import { createInterface } from 'node:readline';
5
+ function printUsage() {
6
+ console.error('Usage: thedesignagent-auth capture <url>');
7
+ console.error('Example: thedesignagent-auth capture http://localhost:3000');
8
+ }
9
+ async function snapshotSession(browser, page, hostname) {
10
+ try {
11
+ const rawCookies = await browser.defaultBrowserContext().cookies();
12
+ const cookies = rawCookies
13
+ .filter(c => c.domain.includes(hostname) || hostname.includes(c.domain.replace(/^\./, '')))
14
+ .map(c => ({
15
+ name: c.name,
16
+ value: c.value,
17
+ domain: c.domain,
18
+ path: c.path,
19
+ httpOnly: c.httpOnly,
20
+ secure: c.secure,
21
+ sameSite: (c.sameSite === 'Strict' || c.sameSite === 'Lax' || c.sameSite === 'None') ? c.sameSite : undefined,
22
+ }));
23
+ const storage = await page.evaluate(() => ({
24
+ localStorage: Object.fromEntries(Object.keys(localStorage).map(k => [k, localStorage.getItem(k) ?? ''])),
25
+ sessionStorage: Object.fromEntries(Object.keys(sessionStorage).map(k => [k, sessionStorage.getItem(k) ?? ''])),
26
+ }));
27
+ return {
28
+ cookies,
29
+ localStorage: storage.localStorage,
30
+ sessionStorage: storage.sessionStorage,
31
+ finalUrl: page.url(),
32
+ };
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function waitForEnter() {
39
+ return new Promise(resolve => {
40
+ if (!process.stdin.isTTY) {
41
+ // No TTY (e.g. backgrounded by an agent) — never resolves. Browser-close will trigger instead.
42
+ return;
43
+ }
44
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
45
+ rl.once('line', () => { rl.close(); resolve('enter'); });
46
+ });
47
+ }
48
+ function waitForBrowserClose(browser) {
49
+ return new Promise(resolve => {
50
+ browser.on('disconnected', () => resolve('closed'));
51
+ });
52
+ }
53
+ // Post-trigger verification: if the browser is still alive after Enter was
54
+ // pressed, navigate back to the original URL and re-snapshot. Catches "user
55
+ // pressed Enter too early" — the partial session would otherwise be saved
56
+ // silently and fail on every subsequent visual call.
57
+ async function verifyAndSnapshot(browser, page, url, trigger, fallback) {
58
+ if (trigger !== 'enter' || !browser.connected)
59
+ return fallback;
60
+ try {
61
+ console.log('Verifying session…');
62
+ await page.goto(url.href, { waitUntil: 'networkidle0', timeout: 15000 });
63
+ const snap = await snapshotSession(browser, page, url.hostname);
64
+ return snap ?? fallback;
65
+ }
66
+ catch {
67
+ return fallback;
68
+ }
69
+ }
70
+ async function main() {
71
+ const [, , subcommand, urlArg] = process.argv;
72
+ if (subcommand !== 'capture' || !urlArg) {
73
+ printUsage();
74
+ process.exit(1);
75
+ }
76
+ let url;
77
+ try {
78
+ url = new URL(urlArg);
79
+ }
80
+ catch {
81
+ console.error(`Invalid URL: ${urlArg}`);
82
+ process.exit(1);
83
+ }
84
+ const chromePath = findChromeExecutable();
85
+ if (!chromePath) {
86
+ console.error('Chrome/Chromium not found. Install Google Chrome (or set CHROME_PATH).');
87
+ process.exit(1);
88
+ }
89
+ const puppeteer = (await import('puppeteer-core')).default;
90
+ console.log(`▌TheDesignAgent▐ auth capture`);
91
+ console.log(`Opening ${url.href} in a browser window…`);
92
+ const browser = await puppeteer.launch({
93
+ executablePath: chromePath,
94
+ headless: false,
95
+ defaultViewport: { width: 1440, height: 900 },
96
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
97
+ });
98
+ const [page] = await browser.pages();
99
+ await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 30000 });
100
+ // Continuously snapshot the session as the user navigates, so we always have
101
+ // a recent capture even if the browser is closed before they confirm.
102
+ let lastSnapshot = null;
103
+ const pollInterval = setInterval(async () => {
104
+ const snap = await snapshotSession(browser, page, url.hostname);
105
+ if (snap)
106
+ lastSnapshot = snap;
107
+ }, 2000);
108
+ const interactiveHint = process.stdin.isTTY
109
+ ? '→ When you\'re logged in, press Enter in your terminal OR close the browser window.'
110
+ : '→ When you\'re logged in, close the browser window. (No TTY detected.)';
111
+ console.log('');
112
+ console.log('→ Log in to your app in the browser window that just opened.');
113
+ console.log(interactiveHint);
114
+ console.log('');
115
+ const trigger = await Promise.race([waitForEnter(), waitForBrowserClose(browser)]);
116
+ clearInterval(pollInterval);
117
+ const verifiedSnapshot = await verifyAndSnapshot(browser, page, url, trigger, lastSnapshot);
118
+ if (trigger !== 'closed') {
119
+ try {
120
+ await browser.close();
121
+ }
122
+ catch { /* already closing */ }
123
+ }
124
+ if (!verifiedSnapshot) {
125
+ console.error('');
126
+ console.error('✗ No session captured — browser may have closed before login completed.');
127
+ console.error(' Re-run the command and log in before closing the window.');
128
+ process.exit(1);
129
+ }
130
+ if (isLoginUrl(verifiedSnapshot.finalUrl)) {
131
+ console.error('');
132
+ console.error('✗ Session not saved — after capture, navigating to the target URL still');
133
+ console.error(' redirected to a login page. This usually means:');
134
+ console.error(' • Login was not fully completed (2FA still pending? form not submitted?)');
135
+ console.error(' • You confirmed before reaching a logged-in page');
136
+ console.error('');
137
+ console.error(` Final URL after verification: ${verifiedSnapshot.finalUrl}`);
138
+ console.error('');
139
+ console.error(' Re-run the command. Make sure you reach a logged-in page in the');
140
+ console.error(' browser before pressing Enter or closing the window.');
141
+ process.exit(1);
142
+ }
143
+ const seed = {
144
+ cookies: verifiedSnapshot.cookies,
145
+ ...(Object.keys(verifiedSnapshot.localStorage).length && { localStorage: verifiedSnapshot.localStorage }),
146
+ ...(Object.keys(verifiedSnapshot.sessionStorage).length && { sessionStorage: verifiedSnapshot.sessionStorage }),
147
+ };
148
+ const savedPath = saveStoredAuth(url.href, seed);
149
+ console.log('');
150
+ console.log(`✓ Trigger: ${trigger === 'closed' ? 'browser closed' : 'Enter pressed'}`);
151
+ console.log(`✓ Captured ${verifiedSnapshot.cookies.length} cookies, ${Object.keys(verifiedSnapshot.localStorage).length} localStorage entries, ${Object.keys(verifiedSnapshot.sessionStorage).length} sessionStorage entries`);
152
+ console.log(`✓ Final URL: ${verifiedSnapshot.finalUrl}`);
153
+ console.log(`✓ Saved to: ${savedPath}`);
154
+ console.log('');
155
+ console.log(`▌TheDesignAgent▐ will now auto-apply this session for any URL on ${url.origin}.`);
156
+ console.log(`To re-capture (after expiry or logout), run this command again.`);
157
+ process.exit(0);
158
+ }
159
+ main().catch(err => {
160
+ console.error('auth-capture failed:', err instanceof Error ? err.message : err);
161
+ process.exit(1);
162
+ });
@@ -0,0 +1,5 @@
1
+ import type { AuthSeed } from './screenshot.js';
2
+ export declare const AUTH_STORE_DIR: string;
3
+ export declare function authFilePath(url: string): string;
4
+ export declare function loadStoredAuth(url: string): AuthSeed | null;
5
+ export declare function saveStoredAuth(url: string, seed: AuthSeed): string;
@@ -0,0 +1,27 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ export const AUTH_STORE_DIR = join(homedir(), '.thedesignagent', 'auth');
5
+ function safeFilename(origin) {
6
+ return origin.replace(/^https?:\/\//, '').replace(/[^a-z0-9.-]/gi, '_') + '.json';
7
+ }
8
+ export function authFilePath(url) {
9
+ return join(AUTH_STORE_DIR, safeFilename(new URL(url).origin));
10
+ }
11
+ export function loadStoredAuth(url) {
12
+ try {
13
+ const path = authFilePath(url);
14
+ if (!existsSync(path))
15
+ return null;
16
+ return JSON.parse(readFileSync(path, 'utf-8'));
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function saveStoredAuth(url, seed) {
23
+ mkdirSync(AUTH_STORE_DIR, { recursive: true });
24
+ const path = authFilePath(url);
25
+ writeFileSync(path, JSON.stringify(seed, null, 2));
26
+ return path;
27
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TheDesignAgent MCP Server
4
+ * Exposes three tools — discover, ux, visual — discoverable by Claude Code, Cursor, etc.
5
+ *
6
+ * Usage in claude_desktop_config.json:
7
+ * {
8
+ * "mcpServers": {
9
+ * "TheDesignAgent": {
10
+ * "command": "npx",
11
+ * "args": ["-y", "--package=@thedesignagent/mcp", "thedesignagent-mcp"],
12
+ * "env": {
13
+ * "THEDESIGNAGENT_API_KEY": "your_key_here",
14
+ * "ANTHROPIC_API_KEY": "your_anthropic_key" // optional: enables prompted fallback
15
+ * }
16
+ * }
17
+ * }
18
+ * }
19
+ */
20
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TheDesignAgent MCP Server
4
+ * Exposes three tools — discover, ux, visual — discoverable by Claude Code, Cursor, etc.
5
+ *
6
+ * Usage in claude_desktop_config.json:
7
+ * {
8
+ * "mcpServers": {
9
+ * "TheDesignAgent": {
10
+ * "command": "npx",
11
+ * "args": ["-y", "--package=@thedesignagent/mcp", "thedesignagent-mcp"],
12
+ * "env": {
13
+ * "THEDESIGNAGENT_API_KEY": "your_key_here",
14
+ * "ANTHROPIC_API_KEY": "your_anthropic_key" // optional: enables prompted fallback
15
+ * }
16
+ * }
17
+ * }
18
+ * }
19
+ */
20
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
21
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
+ import Anthropic from '@anthropic-ai/sdk';
23
+ import { z } from 'zod';
24
+ import { dirname, join } from 'node:path';
25
+ import { captureScreenshot } from './screenshot.js';
26
+ import { loadStoredAuth } from './auth-store.js';
27
+ const API_KEY = process.env.THEDESIGNAGENT_API_KEY;
28
+ const API_BASE = process.env.THEDESIGNAGENT_API_BASE ?? 'https://bxqifcmkydlsgwgcpqda.supabase.co/functions/v1';
29
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
30
+ // ---- Logging --------------------------------------------------------------
31
+ function log(level, step, detail) {
32
+ const ts = new Date().toISOString();
33
+ const line = detail
34
+ ? `[${ts}] [${level.toUpperCase()}] [${step}] ${detail}`
35
+ : `[${ts}] [${level.toUpperCase()}] ${step}`;
36
+ console.error(line);
37
+ }
38
+ // ---- Prompted fallback (ux only) ------------------------------------------
39
+ const FALLBACK_SYSTEM_PROMPT = `You are TheDesignAgent — the UX judgment layer for agent-generated UI, running in degraded fallback mode (the primary pipeline is unavailable).
40
+
41
+ You do NOT have a captured persona or job profile in this mode. Score against a generic professional user; flag this as low-confidence and recommend re-running once the pipeline is healthy.
42
+
43
+ ## The frame
44
+
45
+ Cognitive Load Theory (Sweller, 1988) — three pillars:
46
+ - INTRINSIC: unavoidable effort the task itself demands.
47
+ - EXTRANEOUS: avoidable friction the interface adds (subsumes Quality, Accessibility, Copy issues — they all manifest as extraneous load).
48
+ - GERMANE: productive effort that builds mental models / supports learning.
49
+
50
+ Jobs-to-be-Done (Christensen et al.) — calibrates CLT scores.
51
+
52
+ ## Output format (ONLY valid JSON)
53
+ {
54
+ "ux_score": number (0–10),
55
+ "summary": string (2–3 sentences on overall quality and the most important issue, note fallback mode),
56
+ "findings": [{ "dimension": string, "description": string, "severity": "critical"|"high"|"medium"|"low", "location": string? }],
57
+ "recommendations": [{ "change": string, "reason": string, "priority": "critical"|"high"|"medium"|"low" }],
58
+ "scores": { "heuristics": number, "cognitive": number, "patterns": number, "jtbd_fit": number }
59
+ }
60
+
61
+ findings: 3–8 specific violations. recommendations: 3–6 ordered by priority, actionable.`;
62
+ async function runFallback(task, artifact, codebaseContext) {
63
+ const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
64
+ const contextSection = codebaseContext
65
+ ? `\n\n## Codebase Context\n${codebaseContext.slice(0, 3000)}`
66
+ : '';
67
+ const userMessage = `## Task\n${task}\n\n## Artifact to Evaluate\n${artifact}${contextSection}\n\nReturn ONLY valid JSON matching the schema in the system prompt.`;
68
+ const response = await client.messages.create({
69
+ model: 'claude-opus-4-7',
70
+ max_tokens: 4000,
71
+ system: FALLBACK_SYSTEM_PROMPT,
72
+ messages: [{ role: 'user', content: userMessage }],
73
+ });
74
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
75
+ const jsonStr = text.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim();
76
+ try {
77
+ const parsed = JSON.parse(jsonStr);
78
+ return { ...parsed, status: 'ok', project_id: 'fallback' };
79
+ }
80
+ catch {
81
+ throw new Error(`Fallback Claude returned unparseable JSON. Raw: ${text.slice(0, 200)}`);
82
+ }
83
+ }
84
+ // ---- Output formatting ----------------------------------------------------
85
+ function fmtScore(score) {
86
+ if (score === null || score === undefined || Number.isNaN(score))
87
+ return 'n/a';
88
+ return score % 1 === 0 ? `${score}.0` : `${score}`;
89
+ }
90
+ function formatUxBreakdown(scores) {
91
+ return (`Heuristics: ${fmtScore(scores.heuristics)} · Cognitive: ${fmtScore(scores.cognitive)} · ` +
92
+ `Patterns: ${fmtScore(scores.patterns)} · JTBD fit: ${fmtScore(scores.jtbd_fit)}`);
93
+ }
94
+ function formatVisualBreakdown(scores) {
95
+ return (`Brand: ${fmtScore(scores.brand)} · Visual cognitive: ${fmtScore(scores.visual_cognitive)} · ` +
96
+ `Aesthetic: ${fmtScore(scores.aesthetic)}`);
97
+ }
98
+ function formatFindings(findings) {
99
+ if (!findings?.length)
100
+ return [];
101
+ const lines = ['### Findings'];
102
+ for (const f of findings) {
103
+ const dim = f.dimension ?? f.track ?? 'general';
104
+ const loc = f.location ? ` *(${f.location})*` : '';
105
+ lines.push(`**[${f.severity.toUpperCase()}] ${dim}** — ${f.description}${loc}`, '');
106
+ }
107
+ return lines;
108
+ }
109
+ function formatRecommendations(recs) {
110
+ if (!recs?.length)
111
+ return [];
112
+ const lines = ['### Recommendations (implement before continuing)'];
113
+ for (const r of recs) {
114
+ lines.push(`**[${r.priority.toUpperCase()}]** ${r.change}`, ` *${r.reason}*`, '');
115
+ }
116
+ return lines;
117
+ }
118
+ function formatUxEval(result, isFallback) {
119
+ const parts = [];
120
+ if (isFallback) {
121
+ parts.push('⚠ Pipeline unavailable — evaluation ran in prompted mode (no corpus or project context).', ' Re-run when pipeline is healthy to get corpus-backed results.', '');
122
+ }
123
+ if (result.degradations?.length) {
124
+ parts.push(`⚠ Degraded: ${result.degradations.join(', ')}`, '');
125
+ }
126
+ parts.push(`## ▌TheDesignAgent▐ [ux] Evaluation`, '', `**ux_score: ${fmtScore(result.ux_score)}/10** | ${formatUxBreakdown(result.scores)}`, '', `### Summary`, result.summary, '', ...formatFindings(result.findings), ...formatRecommendations(result.recommendations));
127
+ return parts.join('\n');
128
+ }
129
+ function formatVisualEval(result) {
130
+ const parts = [];
131
+ if (result.degradations?.length) {
132
+ parts.push(`⚠ Degraded: ${result.degradations.join(', ')}`, '');
133
+ }
134
+ parts.push(`## ▌TheDesignAgent▐ [visual] Evaluation`, '', `**visual_score: ${fmtScore(result.visual_score)}/10** | ${formatVisualBreakdown(result.scores)}`, '', `### Summary`, result.summary, '', ...formatFindings(result.findings), ...formatRecommendations(result.recommendations));
135
+ return parts.join('\n');
136
+ }
137
+ function resolveAuthSeed(url, callSeed) {
138
+ if (callSeed)
139
+ return { seed: callSeed, source: 'param' };
140
+ const raw = process.env.THEDESIGNAGENT_AUTH_SEED;
141
+ if (raw) {
142
+ try {
143
+ return { seed: JSON.parse(raw), source: 'env' };
144
+ }
145
+ catch (e) {
146
+ log('warn', 'auth_seed_env_parse_failed', e instanceof Error ? e.message : String(e));
147
+ }
148
+ }
149
+ const stored = loadStoredAuth(url);
150
+ if (stored)
151
+ return { seed: stored, source: 'stored' };
152
+ return { source: 'none' };
153
+ }
154
+ async function tryCaptureScreenshot(url, authSeed) {
155
+ if (!url)
156
+ return {};
157
+ const { seed, source } = resolveAuthSeed(url, authSeed);
158
+ try {
159
+ const shot = await captureScreenshot(url, seed);
160
+ log('info', 'screenshot_captured', `path=${shot.localPath} bytes=${shot.bytes} auth_seed=${source} auth_required=${shot.authRequired} final_url=${shot.finalUrl}`);
161
+ return {
162
+ screenshot: shot.base64,
163
+ screenshotPath: shot.localPath,
164
+ authRequired: shot.authRequired,
165
+ finalUrl: shot.finalUrl,
166
+ authSeedSource: source,
167
+ };
168
+ }
169
+ catch (e) {
170
+ const screenshotError = e instanceof Error ? e.message : String(e);
171
+ log('warn', 'screenshot_failed', screenshotError);
172
+ return { screenshotError, authSeedSource: source };
173
+ }
174
+ }
175
+ function suggestedAuthCmd(renderUrl) {
176
+ try {
177
+ const origin = new URL(renderUrl).origin;
178
+ const authCapturePath = join(dirname(process.argv[1]), 'auth-capture.js');
179
+ return `node ${authCapturePath} capture ${origin}`;
180
+ }
181
+ catch {
182
+ return 'thedesignagent-auth capture <your-app-url>';
183
+ }
184
+ }
185
+ function buildAuthRequiredFooter(renderUrl, capture) {
186
+ const { screenshotPath, finalUrl, authSeedSource } = capture;
187
+ const expired = authSeedSource === 'stored';
188
+ const stateLine = expired
189
+ ? `A saved session for this origin existed but appears expired.`
190
+ : `No saved session for this origin yet.`;
191
+ const lines = [
192
+ `---`,
193
+ `🛑 ▌TheDesignAgent▐ — ACTION REQUIRED: page is behind authentication`,
194
+ ``,
195
+ `The visual eval ran against a login page, not the target screen. The findings above describe the login form and should be DISCARDED.`,
196
+ ``,
197
+ `${stateLine} The user must run this exact command in their own terminal:`,
198
+ ``,
199
+ ` ${suggestedAuthCmd(renderUrl)}`,
200
+ ``,
201
+ `WHAT TO DO NEXT:`,
202
+ `1. Surface the command above to the user verbatim.`,
203
+ `2. Tell them: a Chrome window will open — log in normally, then close it (or press Enter in the terminal).`,
204
+ `3. Wait for them to confirm they've run it.`,
205
+ `4. Then retry this visual call with the same render_url. Auth will be auto-applied.`,
206
+ ``,
207
+ `WHAT NOT TO DO:`,
208
+ `- Do not search the app's source for auth keys, JWT names, or localStorage tokens.`,
209
+ `- Do not attempt to construct auth_seed manually — it almost always fails (HttpOnly cookies are invisible to code search).`,
210
+ `- Do not run the capture command yourself in the background — it needs the user's interactive login.`,
211
+ ];
212
+ if (finalUrl) {
213
+ lines.push(``, `(Diagnostics: redirected to ${finalUrl})`);
214
+ }
215
+ if (screenshotPath) {
216
+ lines.push(`(Login-page screenshot saved at ${screenshotPath} for reference.)`);
217
+ }
218
+ return lines.join('\n');
219
+ }
220
+ function buildVisualFooter(renderUrl, capture) {
221
+ const { screenshotPath, screenshotError, authRequired, authSeedSource } = capture;
222
+ if (authRequired && renderUrl) {
223
+ return buildAuthRequiredFooter(renderUrl, capture);
224
+ }
225
+ if (screenshotPath) {
226
+ const lines = [`---`, `📸 Screenshot: ${screenshotPath}`, ` Source URL: ${renderUrl}`, ` (auto-deleted after 24h)`];
227
+ if (authSeedSource && authSeedSource !== 'none') {
228
+ lines.push(` Auth seed: ${authSeedSource}`);
229
+ }
230
+ return lines.join('\n');
231
+ }
232
+ if (screenshotError && renderUrl) {
233
+ return [`---`, `⚠ Screenshot failed for ${renderUrl}: ${screenshotError}`, ` Eval ran in text-only mode (code analysis).`].join('\n');
234
+ }
235
+ if (renderUrl) {
236
+ return [`---`, `⚠ Screenshot not captured. Eval ran in text-only mode.`].join('\n');
237
+ }
238
+ return '';
239
+ }
240
+ function formatDiscoveryResult(result) {
241
+ if (result.status === 'needs_setup') {
242
+ return [
243
+ `## ▌TheDesignAgent▐ [discover] — Setup Required`,
244
+ '',
245
+ `**project_id:** ${result.project_id}`,
246
+ '',
247
+ `This is the first call for this project. No project model is stored yet.`,
248
+ '',
249
+ `### Extraction Task`,
250
+ result.extraction_task,
251
+ '',
252
+ `Resubmit to \`discover\` with \`project_model\` populated once extraction is complete.`,
253
+ ].join('\n');
254
+ }
255
+ if (result.status === 'model_stale') {
256
+ return [
257
+ `## ▌TheDesignAgent▐ [discover] — Model Stale`,
258
+ '',
259
+ `**project_id:** ${result.project_id}`,
260
+ '',
261
+ `The stored project model's schema_hash no longer matches. Re-extract and resubmit.`,
262
+ '',
263
+ `### Extraction Task`,
264
+ result.extraction_task,
265
+ ].join('\n');
266
+ }
267
+ const ok = result;
268
+ const parts = [
269
+ `## ▌TheDesignAgent▐ [discover] — Build Brief`,
270
+ '',
271
+ `**project_id:** ${ok.project_id} | type: ${ok.project_type} | design tokens: ${ok.has_design_tokens ? 'yes' : 'no'}`,
272
+ `**Active job:** ${ok.active_job}`,
273
+ '',
274
+ ];
275
+ if (ok.degradations?.length) {
276
+ parts.push(`⚠ Degraded: ${ok.degradations.join(', ')}`, '');
277
+ }
278
+ parts.push(ok.brief);
279
+ if (ok.tda_instructions) {
280
+ parts.push('', `---`, `_${ok.tda_instructions}_`);
281
+ }
282
+ return parts.join('\n');
283
+ }
284
+ // ---- Tool handlers --------------------------------------------------------
285
+ async function handleUxCall(args) {
286
+ if (!API_KEY) {
287
+ return {
288
+ content: [{ type: 'text', text: 'THEDESIGNAGENT_API_KEY is not set. Add it to your MCP server config.' }],
289
+ isError: true,
290
+ };
291
+ }
292
+ log('info', 'pipeline_call', `tool=ux project_id=${args.project_id ?? 'none'} task_len=${args.task.length}`);
293
+ let pipelineStatus = 0;
294
+ let pipelineErrorType = 'unknown';
295
+ try {
296
+ const response = await fetch(`${API_BASE}/ux`, {
297
+ method: 'POST',
298
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
299
+ body: JSON.stringify({
300
+ task: args.task,
301
+ artifact: args.artifact,
302
+ project_id: args.project_id,
303
+ repo_hash: args.repo_hash,
304
+ codebaseContext: args.codebase_context,
305
+ ...(args.stages?.length && { stages: args.stages }),
306
+ }),
307
+ });
308
+ pipelineStatus = response.status;
309
+ if (response.ok) {
310
+ const result = await response.json();
311
+ log('info', 'pipeline_ok', `ux_score=${result.ux_score}`);
312
+ return { content: [{ type: 'text', text: formatUxEval(result, false) }] };
313
+ }
314
+ const err = await response.json();
315
+ pipelineErrorType = err.error;
316
+ if (response.status === 401 || response.status === 403) {
317
+ log('error', 'pipeline_auth_failed', `${response.status} ${err.message}`);
318
+ return {
319
+ content: [{ type: 'text', text: `Authentication failed (${response.status}): ${err.message}\nCheck your THEDESIGNAGENT_API_KEY.` }],
320
+ isError: true,
321
+ };
322
+ }
323
+ if (response.status === 429) {
324
+ log('warn', 'pipeline_rate_limited', err.message);
325
+ return {
326
+ content: [{ type: 'text', text: `Rate limit reached: ${err.message}\nUpgrade your plan or wait until your monthly counter resets.` }],
327
+ isError: true,
328
+ };
329
+ }
330
+ if (response.status === 400) {
331
+ log('error', 'pipeline_bad_request', `${err.error}: ${err.message}`);
332
+ return {
333
+ content: [{ type: 'text', text: `Bad request (${err.error}): ${err.message}` }],
334
+ isError: true,
335
+ };
336
+ }
337
+ log('warn', 'pipeline_server_error', `status=${response.status} error=${err.error}`);
338
+ }
339
+ catch (networkErr) {
340
+ pipelineStatus = 0;
341
+ pipelineErrorType = 'network_error';
342
+ log('warn', 'pipeline_network_error', networkErr instanceof Error ? networkErr.message : String(networkErr));
343
+ }
344
+ // Prompted fallback
345
+ if (!ANTHROPIC_API_KEY) {
346
+ log('error', 'fallback_skipped', 'ANTHROPIC_API_KEY not set — cannot run prompted fallback');
347
+ return {
348
+ content: [{
349
+ type: 'text',
350
+ text: [
351
+ `Pipeline unavailable (${pipelineStatus}, ${pipelineErrorType}).`,
352
+ '',
353
+ 'A prompted fallback evaluation is available but requires ANTHROPIC_API_KEY in your MCP config.',
354
+ 'Add it and reload to enable automatic fallback when the pipeline is down.',
355
+ ].join('\n'),
356
+ }],
357
+ isError: true,
358
+ };
359
+ }
360
+ log('info', 'fallback_start', `pipeline_status=${pipelineStatus} error=${pipelineErrorType}`);
361
+ try {
362
+ const fallback = await runFallback(args.task, args.artifact, args.codebase_context);
363
+ log('info', 'fallback_ok', `ux_score=${fallback.ux_score}`);
364
+ return { content: [{ type: 'text', text: formatUxEval(fallback, true) }] };
365
+ }
366
+ catch (fallbackErr) {
367
+ const msg = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
368
+ log('error', 'fallback_failed', msg);
369
+ return {
370
+ content: [{
371
+ type: 'text',
372
+ text: [
373
+ `Pipeline unavailable (${pipelineStatus}, ${pipelineErrorType}) and prompted fallback also failed.`,
374
+ '',
375
+ `Fallback error: ${msg}`,
376
+ '',
377
+ 'Try again in a moment.',
378
+ ].join('\n'),
379
+ }],
380
+ isError: true,
381
+ };
382
+ }
383
+ }
384
+ async function handleVisualCall(args) {
385
+ if (!API_KEY) {
386
+ return {
387
+ content: [{ type: 'text', text: 'THEDESIGNAGENT_API_KEY is not set.' }],
388
+ isError: true,
389
+ };
390
+ }
391
+ log('info', 'pipeline_call', `tool=visual project_id=${args.project_id ?? 'none'} task_len=${args.task.length} render_url=${args.render_url ?? 'none'}`);
392
+ const capture = await tryCaptureScreenshot(args.render_url, args.auth_seed);
393
+ const { screenshot } = capture;
394
+ try {
395
+ const response = await fetch(`${API_BASE}/visual`, {
396
+ method: 'POST',
397
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
398
+ body: JSON.stringify({
399
+ task: args.task,
400
+ artifact: args.artifact,
401
+ project_id: args.project_id,
402
+ repo_hash: args.repo_hash,
403
+ ...(screenshot && { screenshot }),
404
+ ...(args.render_url && { render_url: args.render_url }),
405
+ }),
406
+ });
407
+ if (response.ok) {
408
+ const result = await response.json();
409
+ log('info', 'pipeline_ok', `visual_score=${result.visual_score} auth_required=${capture.authRequired ?? false}`);
410
+ const footer = buildVisualFooter(args.render_url, capture);
411
+ // When auth is required, the eval ran on a login page — its findings are
412
+ // misleading and should not influence the agent. Lead with the action
413
+ // and skip the findings entirely.
414
+ if (capture.authRequired) {
415
+ return { content: [{ type: 'text', text: footer }] };
416
+ }
417
+ const evalText = formatVisualEval(result);
418
+ return { content: [{ type: 'text', text: footer ? `${evalText}\n\n${footer}` : evalText }] };
419
+ }
420
+ const err = await response.json();
421
+ if (response.status === 401 || response.status === 403) {
422
+ return {
423
+ content: [{ type: 'text', text: `Authentication failed (${response.status}): ${err.message}\nCheck your THEDESIGNAGENT_API_KEY.` }],
424
+ isError: true,
425
+ };
426
+ }
427
+ if (response.status === 429) {
428
+ return {
429
+ content: [{ type: 'text', text: `Rate limit reached: ${err.message}` }],
430
+ isError: true,
431
+ };
432
+ }
433
+ log('warn', 'pipeline_server_error', `status=${response.status} error=${err.error} detail=${err.detail ?? 'none'}`);
434
+ const detailLine = err.detail ? `\nDetail: ${err.detail}` : '';
435
+ return {
436
+ content: [{ type: 'text', text: `Pipeline error (${response.status}): ${err.message}${detailLine}` }],
437
+ isError: true,
438
+ };
439
+ }
440
+ catch (networkErr) {
441
+ const msg = networkErr instanceof Error ? networkErr.message : String(networkErr);
442
+ log('warn', 'pipeline_network_error', msg);
443
+ return {
444
+ content: [{ type: 'text', text: `Network error reaching pipeline: ${msg}` }],
445
+ isError: true,
446
+ };
447
+ }
448
+ }
449
+ async function handleDiscoverCall(args) {
450
+ if (!API_KEY) {
451
+ return {
452
+ content: [{ type: 'text', text: 'THEDESIGNAGENT_API_KEY is not set.' }],
453
+ isError: true,
454
+ };
455
+ }
456
+ log('info', 'pipeline_call', `tool=discover project_id=${args.project_id ?? 'none'} task_len=${args.task.length}`);
457
+ try {
458
+ const response = await fetch(`${API_BASE}/discovery`, {
459
+ method: 'POST',
460
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
461
+ body: JSON.stringify({
462
+ task: args.task,
463
+ project_id: args.project_id,
464
+ repo_hash: args.repo_hash,
465
+ codebaseContext: args.codebase_context,
466
+ schemaHash: args.schema_hash,
467
+ ...(args.project_model && { projectModel: args.project_model }),
468
+ ...(args.skills?.length && { skills: args.skills }),
469
+ }),
470
+ });
471
+ if (response.ok) {
472
+ const result = await response.json();
473
+ log('info', 'pipeline_ok', `status=${result.status} project_id=${result.project_id}`);
474
+ return { content: [{ type: 'text', text: formatDiscoveryResult(result) }] };
475
+ }
476
+ const err = await response.json();
477
+ if (response.status === 401 || response.status === 403) {
478
+ return {
479
+ content: [{ type: 'text', text: `Authentication failed (${response.status}): ${err.message}\nCheck your THEDESIGNAGENT_API_KEY.` }],
480
+ isError: true,
481
+ };
482
+ }
483
+ if (response.status === 429) {
484
+ return {
485
+ content: [{ type: 'text', text: `Rate limit reached: ${err.message}` }],
486
+ isError: true,
487
+ };
488
+ }
489
+ log('warn', 'pipeline_server_error', `status=${response.status} error=${err.error} detail=${err.detail ?? 'none'}`);
490
+ const detailLine = err.detail ? `\nDetail: ${err.detail}` : '';
491
+ return {
492
+ content: [{ type: 'text', text: `Pipeline error (${response.status}): ${err.message}${detailLine}` }],
493
+ isError: true,
494
+ };
495
+ }
496
+ catch (networkErr) {
497
+ const msg = networkErr instanceof Error ? networkErr.message : String(networkErr);
498
+ log('warn', 'pipeline_network_error', msg);
499
+ return {
500
+ content: [{ type: 'text', text: `Network error reaching pipeline: ${msg}` }],
501
+ isError: true,
502
+ };
503
+ }
504
+ }
505
+ // ---- MCP server -----------------------------------------------------------
506
+ const evalInputSchema = {
507
+ task: z.string().describe("Describe the workflow or screen you're building"),
508
+ artifact: z.string().describe('The UI description or component code to evaluate'),
509
+ project_id: z.string().optional().describe('project_id from .thedesignagent file in project root'),
510
+ repo_hash: z.string().optional().describe('SHA256 of git remote origin URL (fallback if no .thedesignagent)'),
511
+ codebase_context: z.string().optional().describe('Optional surrounding code context'),
512
+ };
513
+ const server = new McpServer({ name: 'TheDesignAgent', version: '0.2.0' });
514
+ server.registerTool('discover', {
515
+ description: `Get a build brief calibrated to your project, persona, and the specific screen you're building.
516
+
517
+ Call this BEFORE building any screen. TheDesignAgent:
518
+ - Resolves the active Job-to-be-Done for the task
519
+ - Retrieves relevant heuristics, cognitive guidance, and patterns from the corpus
520
+ - Returns a focused build brief (~600 tokens) to inject into your build context
521
+
522
+ On first call for a new project: returns a needs_setup status with an extraction task — scan your repo and resubmit with project_model populated.
523
+ On subsequent calls: returns brief immediately (context is stored).
524
+
525
+ Use project_id from .thedesignagent if available; otherwise pass repo_hash (SHA256 of git remote URL).`,
526
+ inputSchema: {
527
+ task: z.string().describe("Describe the screen you're about to build"),
528
+ project_id: z.string().optional().describe('project_id from .thedesignagent file'),
529
+ repo_hash: z.string().optional().describe('SHA256 of git remote origin URL'),
530
+ codebase_context: z.string().optional().describe('Surrounding code snippet for context'),
531
+ schema_hash: z.string().optional().describe('SHA256 of schema/type files — triggers model refresh if stale'),
532
+ project_model: z.record(z.unknown()).optional().describe('Project model (required on first call or after model_stale)'),
533
+ skills: z.array(z.string()).optional().describe('Per-call skill selection (overrides project defaults)'),
534
+ },
535
+ }, async (args) => handleDiscoverCall(args));
536
+ server.registerTool('ux', {
537
+ description: `Evaluate a built UI artifact for JTBD fit, heuristic compliance, cognitive load, and pattern conformance.
538
+
539
+ Call this AFTER building a screen. TheDesignAgent scores:
540
+ - Heuristics: violations against Nielsen's heuristics + project-specific anti-patterns
541
+ - Cognitive: load profile against the stored persona
542
+ - Patterns: conformance to established project pattern decisions
543
+ - JTBD fit: does the artifact serve the resolved job?
544
+
545
+ Returns ux_score (0–10) + findings + prioritised recommendations.
546
+
547
+ Stores new anti-patterns to project context — future builds benefit automatically.`,
548
+ inputSchema: {
549
+ ...evalInputSchema,
550
+ stages: z.array(z.enum(['heuristics', 'cognitive', 'patterns', 'jtbd-fit'])).optional()
551
+ .describe('Run only named stages (omit to run all)'),
552
+ },
553
+ }, async (args) => handleUxCall(args));
554
+ server.registerTool('visual', {
555
+ description: `Evaluate a built UI artifact for visual design quality — brand compliance, visual cognitive load, and aesthetic quality.
556
+
557
+ Call for visual design review after building. TheDesignAgent scores across three tracks:
558
+ - Brand: color and token compliance against project DESIGN.md
559
+ - Visual cognitive: perceptual hierarchy, focal points, visual complexity
560
+ - Aesthetic: spacing system, type scale, color harmony
561
+
562
+ If render_url is provided, the MCP server takes a real screenshot via headless Chrome and the pipeline runs Vision-based analysis. Otherwise it falls back to text analysis of the artifact code.
563
+
564
+ AUTH-GATED PAGES: If the page is behind login, this tool will detect the redirect and return an auth-required response with a one-line command for the user to run (\`thedesignagent-auth capture <origin>\`). When you receive this response: STOP, surface that exact command to the user, and ask them to retry once they've run it. DO NOT investigate the app's auth scheme. DO NOT attempt to populate auth_seed by grepping for tokens or localStorage keys. The capture command handles all of that automatically; trying to construct auth_seed yourself wastes the user's time and usually doesn't work.
565
+
566
+ Screenshots are saved to ~/.thedesignagent/screenshots/ (24h TTL) so you can open them to see exactly what was evaluated.
567
+
568
+ Returns visual_score (0–10) + findings + prioritised recommendations.`,
569
+ inputSchema: {
570
+ ...evalInputSchema,
571
+ render_url: z.string().optional().describe('Optional URL to screenshot (e.g. http://localhost:3000/checkout). If provided, evaluation uses Claude Vision on the rendered output instead of inferring from code.'),
572
+ auth_seed: z.object({
573
+ cookies: z.array(z.object({
574
+ name: z.string(),
575
+ value: z.string(),
576
+ domain: z.string().optional(),
577
+ path: z.string().optional(),
578
+ httpOnly: z.boolean().optional(),
579
+ secure: z.boolean().optional(),
580
+ sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
581
+ })).optional(),
582
+ localStorage: z.record(z.string()).optional(),
583
+ sessionStorage: z.record(z.string()).optional(),
584
+ }).optional().describe('ADVANCED ESCAPE HATCH for headless/CI use only. For interactive use, do NOT populate this — instead let the tool detect auth redirects and surface the thedesignagent-auth capture command for the user to run. Manually constructing auth_seed almost always fails because production auth uses HttpOnly cookies invisible to your code search.'),
585
+ },
586
+ }, async (args) => handleVisualCall(args));
587
+ const transport = new StdioServerTransport();
588
+ await server.connect(transport);
589
+ log('info', 'server_start', `pipeline=${API_BASE} fallback=${ANTHROPIC_API_KEY ? 'enabled' : 'disabled (set ANTHROPIC_API_KEY to enable)'}`);
@@ -0,0 +1,26 @@
1
+ export declare function findChromeExecutable(): string | null;
2
+ export interface AuthSeedCookie {
3
+ name: string;
4
+ value: string;
5
+ domain?: string;
6
+ path?: string;
7
+ httpOnly?: boolean;
8
+ secure?: boolean;
9
+ sameSite?: 'Strict' | 'Lax' | 'None';
10
+ }
11
+ export interface AuthSeed {
12
+ cookies?: AuthSeedCookie[];
13
+ localStorage?: Record<string, string>;
14
+ sessionStorage?: Record<string, string>;
15
+ }
16
+ export interface ScreenshotResult {
17
+ base64: string;
18
+ localPath: string;
19
+ width: number;
20
+ height: number;
21
+ bytes: number;
22
+ authRequired: boolean;
23
+ finalUrl: string;
24
+ }
25
+ export declare function isLoginUrl(urlStr: string): boolean;
26
+ export declare function captureScreenshot(url: string, authSeed?: AuthSeed): Promise<ScreenshotResult>;
@@ -0,0 +1,167 @@
1
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ const SCREENSHOTS_DIR = join(homedir(), '.thedesignagent', 'screenshots');
6
+ const TTL_MS = 24 * 60 * 60 * 1000;
7
+ const MAC_PATHS = [
8
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
9
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
10
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
11
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
12
+ '/Applications/Arc.app/Contents/MacOS/Arc',
13
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
14
+ ];
15
+ const LINUX_BINS = ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium', 'microsoft-edge', 'brave-browser'];
16
+ const WIN_PATHS = [
17
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
18
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
19
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
20
+ ];
21
+ export function findChromeExecutable() {
22
+ if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) {
23
+ return process.env.CHROME_PATH;
24
+ }
25
+ if (process.platform === 'darwin') {
26
+ for (const p of MAC_PATHS)
27
+ if (existsSync(p))
28
+ return p;
29
+ }
30
+ else if (process.platform === 'linux') {
31
+ for (const bin of LINUX_BINS) {
32
+ try {
33
+ const path = execSync(`which ${bin}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
34
+ if (path && existsSync(path))
35
+ return path;
36
+ }
37
+ catch { /* not installed */ }
38
+ }
39
+ }
40
+ else if (process.platform === 'win32') {
41
+ for (const p of WIN_PATHS)
42
+ if (existsSync(p))
43
+ return p;
44
+ }
45
+ return null;
46
+ }
47
+ function ensureDir() {
48
+ mkdirSync(SCREENSHOTS_DIR, { recursive: true });
49
+ }
50
+ function cleanOldScreenshots() {
51
+ if (!existsSync(SCREENSHOTS_DIR))
52
+ return;
53
+ const cutoff = Date.now() - TTL_MS;
54
+ for (const f of readdirSync(SCREENSHOTS_DIR)) {
55
+ const p = join(SCREENSHOTS_DIR, f);
56
+ try {
57
+ if (statSync(p).mtimeMs < cutoff)
58
+ unlinkSync(p);
59
+ }
60
+ catch { /* ignore */ }
61
+ }
62
+ }
63
+ function safeFilenameSlug(url) {
64
+ return url.replace(/^https?:\/\//, '').replace(/[^a-z0-9]/gi, '-').slice(0, 60);
65
+ }
66
+ const LOGIN_URL_PATTERNS = [
67
+ /\/login(\/|$|\?)/i,
68
+ /\/sign-?in(\/|$|\?)/i,
69
+ /\/auth(\/|$|\?)/i,
70
+ /\/sso(\/|$|\?)/i,
71
+ /\/sessions\/new(\/|$|\?)/i,
72
+ /\/accounts\/login(\/|$|\?)/i,
73
+ ];
74
+ const LOGIN_TITLE_PATTERNS = [
75
+ /sign\s*in/i,
76
+ /log\s*in/i,
77
+ /\blogin\b/i,
78
+ ];
79
+ export function isLoginUrl(urlStr) {
80
+ try {
81
+ const path = new URL(urlStr).pathname;
82
+ return LOGIN_URL_PATTERNS.some(p => p.test(path));
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function detectAuthRequired(requestedUrl, finalUrl, title) {
89
+ // If the user explicitly asked for a login URL, they meant to evaluate it. Don't flag.
90
+ if (isLoginUrl(requestedUrl))
91
+ return false;
92
+ return isLoginUrl(finalUrl) || LOGIN_TITLE_PATTERNS.some(p => p.test(title));
93
+ }
94
+ export async function captureScreenshot(url, authSeed) {
95
+ ensureDir();
96
+ cleanOldScreenshots();
97
+ const chromePath = findChromeExecutable();
98
+ if (!chromePath) {
99
+ throw new Error('Chrome/Chromium not found. Install Google Chrome (or Edge/Brave/Chromium), or set CHROME_PATH env var to your browser binary.');
100
+ }
101
+ // Lazy import so the MCP server still starts if puppeteer-core isn't installed.
102
+ const puppeteer = (await import('puppeteer-core')).default;
103
+ const browser = await puppeteer.launch({
104
+ executablePath: chromePath,
105
+ headless: true,
106
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
107
+ });
108
+ try {
109
+ const page = await browser.newPage();
110
+ await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
111
+ if (authSeed) {
112
+ await applyAuthSeed(page, url, authSeed);
113
+ }
114
+ await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
115
+ // Brief settle for hydration / animations
116
+ await new Promise(r => setTimeout(r, 500));
117
+ const finalUrl = page.url();
118
+ const title = await page.title();
119
+ const authRequired = detectAuthRequired(url, finalUrl, title);
120
+ const buffer = Buffer.from(await page.screenshot({
121
+ type: 'jpeg',
122
+ quality: 88,
123
+ fullPage: true,
124
+ }));
125
+ const ts = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-');
126
+ const slug = safeFilenameSlug(url);
127
+ const filename = `${ts}__${slug}.jpg`;
128
+ const localPath = join(SCREENSHOTS_DIR, filename);
129
+ writeFileSync(localPath, buffer);
130
+ return {
131
+ base64: buffer.toString('base64'),
132
+ localPath,
133
+ width: 1440,
134
+ height: 900,
135
+ bytes: buffer.length,
136
+ authRequired,
137
+ finalUrl,
138
+ };
139
+ }
140
+ finally {
141
+ await browser.close();
142
+ }
143
+ }
144
+ async function applyAuthSeed(page, url, seed) {
145
+ const { hostname } = new URL(url);
146
+ if (seed.cookies?.length) {
147
+ const cookies = seed.cookies.map(c => ({
148
+ ...c,
149
+ // Default cookies to the target hostname if domain not provided.
150
+ domain: c.domain ?? hostname,
151
+ }));
152
+ await page.browserContext().setCookie(...cookies);
153
+ }
154
+ if (seed.localStorage || seed.sessionStorage) {
155
+ const ls = seed.localStorage ?? {};
156
+ const ss = seed.sessionStorage ?? {};
157
+ await page.evaluateOnNewDocument((lsEntries, ssEntries) => {
158
+ try {
159
+ for (const [k, v] of lsEntries)
160
+ localStorage.setItem(k, v);
161
+ for (const [k, v] of ssEntries)
162
+ sessionStorage.setItem(k, v);
163
+ }
164
+ catch { /* storage unavailable on some origins; ignore */ }
165
+ }, Object.entries(ls), Object.entries(ss));
166
+ }
167
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@thedesignagent/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "MCP server for TheDesignAgent — UX and visual judgment layer for agent-generated UI",
5
+ "type": "module",
6
+ "homepage": "https://thedesignagent.ai",
7
+ "bin": {
8
+ "thedesignagent-mcp": "./dist/index.js",
9
+ "thedesignagent-auth": "./dist/auth-capture.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsx index.ts",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "@anthropic-ai/sdk": "^0.36.0",
27
+ "@modelcontextprotocol/sdk": "^1.0.0",
28
+ "puppeteer-core": "^23.11.1",
29
+ "zod": "^3.22.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.0.0",
33
+ "tsx": "^4.0.0",
34
+ "typescript": "^5.0.0"
35
+ }
36
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.