@thedesignagent/mcp 0.2.5 → 0.4.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,8 @@
1
+ export declare const CREDENTIALS_PATH: string;
2
+ export declare function looksLikeApiKey(key: string): boolean;
3
+ export declare function loadApiKey(): {
4
+ key?: string;
5
+ source: 'env' | 'file' | 'none';
6
+ };
7
+ export declare function saveApiKey(key: string): string;
8
+ export declare const MISSING_KEY_HELP: string;
@@ -0,0 +1,44 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ // One key file for every client: MCP hosts with no standard way to prompt for
5
+ // a key (Agent Plugins, Cursor, Copilot) and the CLI both read it. The env var
6
+ // still wins, so CI and existing MCP configs behave as before.
7
+ export const CREDENTIALS_PATH = join(homedir(), '.thedesignagent', 'credentials');
8
+ export function looksLikeApiKey(key) {
9
+ return /^tda_[A-Za-z0-9_-]{8,}$/.test(key);
10
+ }
11
+ export function loadApiKey() {
12
+ const fromEnv = process.env.THEDESIGNAGENT_API_KEY?.trim();
13
+ if (fromEnv)
14
+ return { key: fromEnv, source: 'env' };
15
+ try {
16
+ if (!existsSync(CREDENTIALS_PATH))
17
+ return { source: 'none' };
18
+ const parsed = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf-8'));
19
+ if (typeof parsed.api_key === 'string' && parsed.api_key.trim()) {
20
+ return { key: parsed.api_key.trim(), source: 'file' };
21
+ }
22
+ }
23
+ catch {
24
+ // Unreadable or malformed: treat as missing; `login` rewrites it.
25
+ }
26
+ return { source: 'none' };
27
+ }
28
+ export function saveApiKey(key) {
29
+ const dir = dirname(CREDENTIALS_PATH);
30
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
31
+ // Write a fresh 0600 file and rename it over the old one, so the key is
32
+ // never briefly readable through an existing file's looser mode.
33
+ const tmp = `${CREDENTIALS_PATH}.${process.pid}.tmp`;
34
+ writeFileSync(tmp, JSON.stringify({ api_key: key }, null, 2) + '\n', { mode: 0o600 });
35
+ chmodSync(tmp, 0o600);
36
+ renameSync(tmp, CREDENTIALS_PATH);
37
+ return CREDENTIALS_PATH;
38
+ }
39
+ export const MISSING_KEY_HELP = [
40
+ 'No TheDesignAgent API key found.',
41
+ 'Get one at https://thedesignagent.ai/dashboard/api-keys, then either:',
42
+ ' - run: npx -y --package=@thedesignagent/mcp thedesignagent login',
43
+ ' - or set the THEDESIGNAGENT_API_KEY environment variable.',
44
+ ].join('\n');
package/dist/index.js CHANGED
@@ -24,10 +24,14 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
24
24
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
25
  import Anthropic from '@anthropic-ai/sdk';
26
26
  import { z } from 'zod';
27
+ import { existsSync } from 'node:fs';
27
28
  import { dirname, join } from 'node:path';
28
29
  import { captureScreenshot } from './screenshot.js';
29
30
  import { loadStoredAuth } from './auth-store.js';
30
- const API_KEY = process.env.THEDESIGNAGENT_API_KEY;
31
+ import { loadApiKey, MISSING_KEY_HELP } from './credentials.js';
32
+ // Read per call: a key saved with `thedesignagent login` after the server
33
+ // started still takes effect without a restart.
34
+ const apiKey = () => loadApiKey().key;
31
35
  const API_BASE = process.env.THEDESIGNAGENT_API_BASE ?? 'https://bxqifcmkydlsgwgcpqda.supabase.co/functions/v1';
32
36
  // Discover/Ux route through the hosted gateway so mcp_call_logs telemetry
33
37
  // captures local-stdio calls too. Visual stays direct because it needs local
@@ -40,7 +44,7 @@ async function callGatewayTool(name, args) {
40
44
  response = await fetch(`${GATEWAY_BASE}/mcp`, {
41
45
  method: 'POST',
42
46
  headers: {
43
- Authorization: `Bearer ${API_KEY}`,
47
+ Authorization: `Bearer ${apiKey()}`,
44
48
  'Content-Type': 'application/json',
45
49
  'Accept': 'application/json',
46
50
  },
@@ -111,13 +115,24 @@ async function runFallback(task, artifact, codebaseContext) {
111
115
  ? `\n\n## Codebase Context\n${codebaseContext.slice(0, 3000)}`
112
116
  : '';
113
117
  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.`;
118
+ // Opus 5.5 always thinks: thinking shares max_tokens, and the reply is not
119
+ // content[0]. `fallbacks` (not yet in the SDK types) re-runs a safety
120
+ // classifier decline on Anthropic's recommended model within the same call.
114
121
  const response = await client.messages.create({
115
- model: 'claude-opus-4-7',
116
- max_tokens: 4000,
122
+ model: 'claude-opus-5-5',
123
+ max_tokens: 16000,
124
+ output_config: { effort: 'medium' },
117
125
  system: FALLBACK_SYSTEM_PROMPT,
118
126
  messages: [{ role: 'user', content: userMessage }],
119
- });
120
- const text = response.content[0].type === 'text' ? response.content[0].text : '';
127
+ fallbacks: 'default',
128
+ }, { headers: { 'anthropic-beta': 'server-side-fallback-2026-07-01' } });
129
+ if (response.stop_reason === 'refusal') {
130
+ throw new Error('Fallback Claude declined the request');
131
+ }
132
+ const text = response.content
133
+ .filter((b) => b.type === 'text')
134
+ .map((b) => b.text)
135
+ .join('');
121
136
  const jsonStr = text.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim();
122
137
  try {
123
138
  const parsed = JSON.parse(jsonStr);
@@ -218,43 +233,43 @@ async function tryCaptureScreenshot(url, authSeed) {
218
233
  return { screenshotError, authSeedSource: source };
219
234
  }
220
235
  }
221
- function suggestedAuthCmd(renderUrl) {
222
- try {
223
- const origin = new URL(renderUrl).origin;
224
- const authCapturePath = join(dirname(process.argv[1]), 'auth-capture.js');
225
- return `node ${authCapturePath} capture ${origin}`;
226
- }
227
- catch {
228
- return 'thedesignagent-auth capture <your-app-url>';
229
- }
236
+ // The login recipe a repo can provide (written by the plugin's setup flow).
237
+ // It prints either a URL that logs in when opened or an AuthSeed JSON.
238
+ const LOGIN_RECIPE = '.thedesignagent-login.mjs';
239
+ // Run the auth CLI that ships alongside this server, so its version always matches.
240
+ // Pass the page being reviewed, not just the origin: many apps 404 or
241
+ // redirect at the root, and the session is saved per origin either way.
242
+ function authCmd(sub, pageUrl) {
243
+ const authCapturePath = join(dirname(process.argv[1]), 'auth-capture.js');
244
+ return `node "${authCapturePath}" ${sub} ${pageUrl}`;
230
245
  }
231
246
  function buildAuthRequiredFooter(renderUrl, capture) {
232
247
  const { screenshotPath, finalUrl, authSeedSource } = capture;
233
- const expired = authSeedSource === 'stored';
234
- const stateLine = expired
235
- ? `A saved session for this origin existed but appears expired.`
236
- : `No saved session for this origin yet.`;
248
+ const origin = (() => { try {
249
+ return new URL(renderUrl).origin;
250
+ }
251
+ catch {
252
+ return renderUrl;
253
+ } })();
254
+ const stateLine = authSeedSource === 'stored'
255
+ ? `A saved session for ${origin} existed but has expired.`
256
+ : `No saved session for ${origin} yet.`;
257
+ const hasRecipe = existsSync(join(process.cwd(), LOGIN_RECIPE));
237
258
  const lines = [
238
259
  `---`,
239
- `🛑 ▌TheDesignAgent▐ — ACTION REQUIRED: page is behind authentication`,
260
+ `🛑 ▌TheDesignAgent▐ — page is behind login. No review ran and no credits were charged.`,
240
261
  ``,
241
- `The visual eval ran against a login page, not the target screen. The findings above describe the login form and should be DISCARDED.`,
242
- ``,
243
- `${stateLine} The user must run this exact command in their own terminal:`,
244
- ``,
245
- ` ${suggestedAuthCmd(renderUrl)}`,
262
+ stateLine,
246
263
  ``,
247
264
  `WHAT TO DO NEXT:`,
248
- `1. Surface the command above to the user verbatim.`,
249
- `2. Tell them: a Chrome window will open — log in normally, then close it (or press Enter in the terminal).`,
250
- `3. Wait for them to confirm they've run it.`,
251
- `4. Then retry this visual call with the same render_url. Auth will be auto-applied.`,
252
- ``,
253
- `WHAT NOT TO DO:`,
254
- `- Do not search the app's source for auth keys, JWT names, or localStorage tokens.`,
255
- `- Do not attempt to construct auth_seed manually — it almost always fails (HttpOnly cookies are invisible to code search).`,
256
- `- Do not run the capture command yourself in the background — it needs the user's interactive login.`,
257
265
  ];
266
+ if (hasRecipe) {
267
+ lines.push(`1. This repo has a login recipe. Run it (from the repo root):`, ` node ${LOGIN_RECIPE} | ${authCmd('login', renderUrl)}`, `2. When it prints TDA_AUTH_SAVED, retry this Visual call with the same render_url.`, `3. If it prints TDA_AUTH_FAILED, fall back to the manual capture below.`, ``, `Manual capture (fallback):`);
268
+ }
269
+ lines.push(`${hasRecipe ? '' : '1. '}Run this yourself, in the background (it opens a browser window for the user):`, ` ${authCmd('capture', renderUrl)}`, ` Tell the user: "A browser window opened at ${origin}. Log in there; it saves and closes by itself."`, ` Wait for the command to finish. It prints TDA_AUTH_SAVED when done (or TDA_AUTH_FAILED with a reason; it gives up after 10 minutes).`, `${hasRecipe ? '' : '2. '}Then retry this Visual call once with the same render_url. The session is applied automatically.`, ``, `WHAT NOT TO DO:`, `- Do not search the app's source for auth keys, JWT names, or localStorage tokens.`, `- Do not construct auth_seed by hand. HttpOnly cookies are invisible to code search, so it almost always fails.`);
270
+ if (!hasRecipe) {
271
+ lines.push(``, `To make this automatic next time, the plugin's setup flow can write a ${LOGIN_RECIPE} for this repo.`);
272
+ }
258
273
  if (finalUrl) {
259
274
  lines.push(``, `(Diagnostics: redirected to ${finalUrl})`);
260
275
  }
@@ -329,9 +344,9 @@ function formatDiscoveryResult(result) {
329
344
  }
330
345
  // ---- Tool handlers --------------------------------------------------------
331
346
  async function handleUxCall(args) {
332
- if (!API_KEY) {
347
+ if (!apiKey()) {
333
348
  return {
334
- content: [{ type: 'text', text: 'THEDESIGNAGENT_API_KEY is not set. Add it to your MCP server config.' }],
349
+ content: [{ type: 'text', text: MISSING_KEY_HELP }],
335
350
  isError: true,
336
351
  };
337
352
  }
@@ -409,19 +424,25 @@ async function handleUxCall(args) {
409
424
  }
410
425
  }
411
426
  async function handleVisualCall(args) {
412
- if (!API_KEY) {
427
+ if (!apiKey()) {
413
428
  return {
414
- content: [{ type: 'text', text: 'THEDESIGNAGENT_API_KEY is not set.' }],
429
+ content: [{ type: 'text', text: MISSING_KEY_HELP }],
415
430
  isError: true,
416
431
  };
417
432
  }
418
433
  log('info', 'pipeline_call', `tool=visual project_id=${args.project_id ?? 'none'} task_len=${args.task.length} render_url=${args.render_url ?? 'none'}`);
419
434
  const capture = await tryCaptureScreenshot(args.render_url, args.auth_seed);
420
435
  const { screenshot } = capture;
436
+ // Landed on a login page: stop before the pipeline. Evaluating the login
437
+ // form would cost credits and produce findings the agent must discard.
438
+ if (capture.authRequired && args.render_url) {
439
+ log('info', 'visual_auth_required', `render_url=${args.render_url} final_url=${capture.finalUrl ?? 'unknown'}`);
440
+ return { content: [{ type: 'text', text: buildAuthRequiredFooter(args.render_url, capture) }] };
441
+ }
421
442
  try {
422
443
  const response = await fetch(`${API_BASE}/visual`, {
423
444
  method: 'POST',
424
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
445
+ headers: { Authorization: `Bearer ${apiKey()}`, 'Content-Type': 'application/json' },
425
446
  body: JSON.stringify({
426
447
  task: args.task,
427
448
  artifact: args.artifact,
@@ -447,7 +468,7 @@ async function handleVisualCall(args) {
447
468
  const err = await response.json();
448
469
  if (response.status === 401 || response.status === 403) {
449
470
  return {
450
- content: [{ type: 'text', text: `Authentication failed (${response.status}): ${err.message}\nCheck your THEDESIGNAGENT_API_KEY.` }],
471
+ content: [{ type: 'text', text: `Authentication failed (${response.status}): ${err.message}\nCheck your API key (THEDESIGNAGENT_API_KEY or "thedesignagent login").` }],
451
472
  isError: true,
452
473
  };
453
474
  }
@@ -474,9 +495,9 @@ async function handleVisualCall(args) {
474
495
  }
475
496
  }
476
497
  async function handleDiscoverCall(args) {
477
- if (!API_KEY) {
498
+ if (!apiKey()) {
478
499
  return {
479
- content: [{ type: 'text', text: 'THEDESIGNAGENT_API_KEY is not set.' }],
500
+ content: [{ type: 'text', text: MISSING_KEY_HELP }],
480
501
  isError: true,
481
502
  };
482
503
  }
@@ -516,7 +537,7 @@ const evalInputSchema = {
516
537
  repo_hash: z.string().optional().describe('SHA256 of git remote origin URL (fallback if no .thedesignagent)'),
517
538
  codebase_context: z.string().optional().describe('Optional surrounding code context'),
518
539
  };
519
- const server = new McpServer({ name: 'TheDesignAgent', version: '0.2.5' });
540
+ const server = new McpServer({ name: 'TheDesignAgent', version: '0.4.0' });
520
541
  server.registerTool('Discover', {
521
542
  description: `Get a build brief calibrated to your project, persona, and the specific screen you're building.
522
543
 
@@ -567,7 +588,7 @@ Call for visual design review after building. TheDesignAgent scores across three
567
588
 
568
589
  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.
569
590
 
570
- 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.
591
+ AUTH-GATED PAGES: If the page is behind login, this tool stops before evaluating (no credits charged) and returns the exact commands to save a session: a repo login recipe (.thedesignagent-login.mjs) when one exists, otherwise a browser capture you run in the background while the user logs in (it saves and closes by itself). Follow those instructions, then retry once. DO NOT investigate the app's auth scheme or populate auth_seed by grepping for tokens or localStorage keys; that wastes the user's time and usually doesn't work.
571
592
 
572
593
  Screenshots are saved to ~/.thedesignagent/screenshots/ (24h TTL) so you can open them to see exactly what was evaluated.
573
594
 
@@ -7,6 +7,7 @@ export interface AuthSeedCookie {
7
7
  httpOnly?: boolean;
8
8
  secure?: boolean;
9
9
  sameSite?: 'Strict' | 'Lax' | 'None';
10
+ expires?: number;
10
11
  }
11
12
  export interface AuthSeed {
12
13
  cookies?: AuthSeedCookie[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thedesignagent/mcp",
3
- "version": "0.2.5",
4
- "description": "MCP server for TheDesignAgent — UX and visual judgment layer for agent-generated UI",
3
+ "version": "0.4.0",
4
+ "description": "MCP server and CLI for TheDesignAgent \u2014 UX and visual judgment layer for agent-generated UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "homepage": "https://thedesignagent.ai",
@@ -14,11 +14,14 @@
14
14
  "design-system",
15
15
  "claude-code",
16
16
  "codex",
17
- "ai-agents"
17
+ "ai-agents",
18
+ "cli",
19
+ "ci"
18
20
  ],
19
21
  "bin": {
20
22
  "thedesignagent-mcp": "dist/index.js",
21
- "thedesignagent-auth": "dist/auth-capture.js"
23
+ "thedesignagent-auth": "dist/auth-capture.js",
24
+ "thedesignagent": "dist/cli.js"
22
25
  },
23
26
  "files": [
24
27
  "dist"
@@ -35,7 +38,7 @@
35
38
  "prepublishOnly": "npm run build"
36
39
  },
37
40
  "dependencies": {
38
- "@anthropic-ai/sdk": "^0.36.0",
41
+ "@anthropic-ai/sdk": "^0.92.0",
39
42
  "@modelcontextprotocol/sdk": "^1.0.0",
40
43
  "puppeteer-core": "^23.11.1",
41
44
  "zod": "^3.22.0"