@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.
- package/README.md +157 -5
- package/dist/auth-capture.js +245 -110
- package/dist/auth-store.d.ts +1 -0
- package/dist/auth-store.js +7 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +771 -0
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.js +44 -0
- package/dist/index.js +65 -44
- package/dist/screenshot.d.ts +1 -0
- package/package.json +8 -5
|
@@ -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
|
-
|
|
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 ${
|
|
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-
|
|
116
|
-
max_tokens:
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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▐ —
|
|
260
|
+
`🛑 ▌TheDesignAgent▐ — page is behind login. No review ran and no credits were charged.`,
|
|
240
261
|
``,
|
|
241
|
-
|
|
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 (!
|
|
347
|
+
if (!apiKey()) {
|
|
333
348
|
return {
|
|
334
|
-
content: [{ type: 'text', text:
|
|
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 (!
|
|
427
|
+
if (!apiKey()) {
|
|
413
428
|
return {
|
|
414
|
-
content: [{ type: 'text', text:
|
|
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 ${
|
|
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 (!
|
|
498
|
+
if (!apiKey()) {
|
|
478
499
|
return {
|
|
479
|
-
content: [{ type: 'text', text:
|
|
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.
|
|
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
|
|
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
|
|
package/dist/screenshot.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thedesignagent/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for TheDesignAgent
|
|
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.
|
|
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"
|