circuit-mcp 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,12 +5,20 @@ Connect Circuit to Cursor and Claude Code via the Model Context Protocol.
5
5
 
6
6
  ## Setup
7
7
 
8
+ Sign in first — this opens your browser and caches a token at `~/.circuit/token.json` (30-day expiry, auto-refresh):
9
+
10
+ ```bash
11
+ npx circuit-mcp auth
12
+ ```
13
+
8
14
  ### Claude Code
9
15
 
10
16
  ```bash
11
- claude mcp add circuit -- npx circuit-mcp
17
+ claude mcp add --scope user circuit -- npx circuit-mcp
12
18
  ```
13
19
 
20
+ `--scope user` makes Circuit available in every project, not just the directory you ran the command from.
21
+
14
22
  ### Cursor
15
23
 
16
24
  Add to `~/.cursor/mcp.json`:
@@ -26,7 +34,7 @@ Add to `~/.cursor/mcp.json`:
26
34
  }
27
35
  ```
28
36
 
29
- First run opens your browser to authenticate. Token is cached at `~/.circuit/token.json` (30-day expiry).
37
+ If you add the server before signing in, it exits with a reminder to run `npx circuit-mcp auth` — sign in, then reconnect from your editor.
30
38
 
31
39
  ---
32
40
 
File without changes
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "circuit-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Connect Circuit to Cursor and Claude Code - bring customer priorities and engineering briefs into your AI coding assistant",
5
5
  "type": "module",
6
6
  "bin": {
7
- "circuit-mcp": "./bin/circuit-mcp.js"
7
+ "circuit-mcp": "bin/circuit-mcp.js"
8
8
  },
9
9
  "main": "./src/index.js",
10
10
  "exports": "./src/index.js",
@@ -15,7 +15,8 @@
15
15
  "README.md"
16
16
  ],
17
17
  "scripts": {
18
- "start": "node bin/circuit-mcp.js"
18
+ "start": "node bin/circuit-mcp.js",
19
+ "test": "node --test"
19
20
  },
20
21
  "keywords": [
21
22
  "circuit",
@@ -36,7 +37,7 @@
36
37
  "homepage": "https://withcircuit.com",
37
38
  "repository": {
38
39
  "type": "git",
39
- "url": "https://github.com/withcircuit/Circuit.git",
40
+ "url": "git+https://github.com/withcircuit/Circuit.git",
40
41
  "directory": "circuit-mcp"
41
42
  },
42
43
  "bugs": {
package/src/auth.js CHANGED
@@ -7,6 +7,7 @@ import os from 'os';
7
7
  import open from 'open';
8
8
  import chalk from 'chalk';
9
9
  import { showSpinner, showInfo, showPrompt } from './ui.js';
10
+ import { getSuccessPage, getErrorPage } from './pages.js';
10
11
 
11
12
  const CIRCUIT_URL = process.env.CIRCUIT_APP_URL || 'https://app.withcircuit.com';
12
13
  const TOKEN_FILE = path.join(os.homedir(), '.circuit', 'token.json');
@@ -178,131 +179,3 @@ export async function authenticate() {
178
179
  server.on('error', reject);
179
180
  });
180
181
  }
181
-
182
- function getSuccessPage() {
183
- return `<!DOCTYPE html>
184
- <html>
185
- <head>
186
- <title>Circuit - Connected</title>
187
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'><defs><mask id='m'><circle cx='16' cy='16' r='13' fill='white'/><circle cx='16' cy='16' r='6.9' fill='black'/></mask></defs><ellipse cx='16' cy='17.73' rx='13' ry='11.27' fill='rgba(168,102,122,0.2)'/><circle cx='16' cy='16' r='13' fill='%23A8667A' mask='url(%23m)'/></svg>">
188
- <style>
189
- * { margin: 0; padding: 0; box-sizing: border-box; }
190
- body {
191
- font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
192
- background: #F5F3F0;
193
- min-height: 100vh;
194
- display: flex;
195
- align-items: center;
196
- justify-content: center;
197
- color: #1C1A18;
198
- padding: 24px;
199
- }
200
- .content {
201
- text-align: center;
202
- max-width: 400px;
203
- }
204
- .icon {
205
- margin-bottom: 24px;
206
- }
207
- .icon svg {
208
- width: 48px;
209
- height: 48px;
210
- color: #1C1A18;
211
- }
212
- h1 {
213
- font-size: 24px;
214
- font-weight: 600;
215
- color: #1C1A18;
216
- margin-bottom: 12px;
217
- }
218
- p {
219
- font-size: 14px;
220
- color: rgba(28, 26, 24, 0.6);
221
- line-height: 1.6;
222
- }
223
- .hint {
224
- font-size: 12px;
225
- color: rgba(28, 26, 24, 0.6);
226
- margin-top: 16px;
227
- }
228
- </style>
229
- </head>
230
- <body>
231
- <div class="content">
232
- <div class="icon">
233
- <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
234
- </div>
235
- <h1>Connected.</h1>
236
- <p>Your AI coding assistant is now connected to Circuit.</p>
237
- <p class="hint">You can close this tab and return to your editor.</p>
238
- </div>
239
- <script>setTimeout(() => window.close(), 3000);</script>
240
- </body>
241
- </html>`;
242
- }
243
-
244
- function getErrorPage(error) {
245
- // Sanitize error message to prevent XSS in the static HTML page
246
- const safeError = String(error).replace(/[<>&"']/g, c => ({
247
- '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;'
248
- })[c]);
249
-
250
- return `<!DOCTYPE html>
251
- <html>
252
- <head>
253
- <title>Circuit - Connection Failed</title>
254
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'><defs><mask id='m'><circle cx='16' cy='16' r='13' fill='white'/><circle cx='16' cy='16' r='6.9' fill='black'/></mask></defs><ellipse cx='16' cy='17.73' rx='13' ry='11.27' fill='rgba(168,102,122,0.2)'/><circle cx='16' cy='16' r='13' fill='%23A8667A' mask='url(%23m)'/></svg>">
255
- <style>
256
- * { margin: 0; padding: 0; box-sizing: border-box; }
257
- body {
258
- font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
259
- background: #F5F3F0;
260
- min-height: 100vh;
261
- display: flex;
262
- align-items: center;
263
- justify-content: center;
264
- color: #1C1A18;
265
- padding: 24px;
266
- }
267
- .content {
268
- text-align: center;
269
- max-width: 400px;
270
- }
271
- .icon {
272
- margin-bottom: 24px;
273
- }
274
- .icon svg {
275
- width: 48px;
276
- height: 48px;
277
- color: #D64545;
278
- }
279
- h1 {
280
- font-size: 24px;
281
- font-weight: 600;
282
- color: #1C1A18;
283
- margin-bottom: 12px;
284
- }
285
- p {
286
- font-size: 14px;
287
- color: rgba(28, 26, 24, 0.6);
288
- line-height: 1.6;
289
- }
290
- .hint {
291
- font-size: 12px;
292
- color: rgba(28, 26, 24, 0.6);
293
- margin-top: 16px;
294
- }
295
- </style>
296
- </head>
297
- <body>
298
- <div class="content">
299
- <div class="icon">
300
- <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
301
- </div>
302
- <h1>Connection Failed</h1>
303
- <p>${safeError}</p>
304
- <p class="hint">Please try connecting again from your AI assistant.</p>
305
- </div>
306
- </body>
307
- </html>`;
308
- }
package/src/index.js CHANGED
@@ -92,7 +92,7 @@ function showTroubleshoot() {
92
92
  console.log(chalk.dim(' 1. Check if MCP is installed:'));
93
93
  console.log(chalk.white(' claude mcp list\n'));
94
94
  console.log(chalk.dim(' 2. If not listed, add it:'));
95
- console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
95
+ console.log(chalk.white(' claude mcp add --scope user circuit -- npx circuit-mcp\n'));
96
96
  console.log(chalk.dim(' 3. Restart Claude Code\n'));
97
97
 
98
98
  // Issue 4: Authentication issues
@@ -169,6 +169,7 @@ async function configureCursor() {
169
169
  await fs.mkdir(path.dirname(configPath), { recursive: true });
170
170
 
171
171
  let config = { mcpServers: {} };
172
+ let wasDisabled = false;
172
173
 
173
174
  // Try to read existing config
174
175
  try {
@@ -177,31 +178,40 @@ async function configureCursor() {
177
178
  if (!config.mcpServers) {
178
179
  config.mcpServers = {};
179
180
  }
181
+ // Check if Circuit was previously disabled in Cursor
182
+ wasDisabled = config.mcpServers.circuit?.disabled === true;
180
183
  } catch {
181
184
  // File doesn't exist, use default
182
185
  }
183
186
 
184
- // Add Circuit
187
+ // Write Circuit config — overwrites any previous disabled: true
185
188
  config.mcpServers.circuit = circuitConfig;
186
189
 
187
190
  // Write config
188
191
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
189
- return true;
192
+ return { success: true, wasDisabled };
190
193
  } catch (err) {
191
- return false;
194
+ return { success: false, wasDisabled: false };
192
195
  }
193
196
  }
194
197
 
195
198
  // Configure Claude Code by running the CLI command
196
199
  function configureClaudeCode() {
197
200
  try {
198
- execSync('claude mcp add circuit -- npx circuit-mcp', {
201
+ // --scope user makes Circuit available in every Claude Code session,
202
+ // not just the current project directory (default scope is "local").
203
+ // Remove first so re-running setup always produces a clean state.
204
+ try {
205
+ execSync('claude mcp remove circuit', { stdio: 'pipe', timeout: 5000 });
206
+ } catch {
207
+ // Not present yet — fine
208
+ }
209
+ execSync('claude mcp add --scope user circuit -- npx circuit-mcp', {
199
210
  stdio: 'pipe',
200
211
  timeout: 10000
201
212
  });
202
213
  return true;
203
214
  } catch (err) {
204
- // Command might fail if claude CLI not installed
205
215
  return false;
206
216
  }
207
217
  }
@@ -226,10 +236,15 @@ async function runSetup() {
226
236
  if (choice === '1' || choice === '3') {
227
237
  console.log(chalk.dim(' Setting up Cursor...\n'));
228
238
 
229
- const success = await configureCursor();
239
+ const { success, wasDisabled } = await configureCursor();
230
240
 
231
241
  if (success) {
232
- showSuccess('Added Circuit to ~/.cursor/mcp.json');
242
+ if (wasDisabled) {
243
+ showSuccess('Re-enabled Circuit in ~/.cursor/mcp.json');
244
+ console.log(chalk.dim(' (It was disabled — turned back on.)\n'));
245
+ } else {
246
+ showSuccess('Added Circuit to ~/.cursor/mcp.json');
247
+ }
233
248
  console.log(chalk.dim(' Restart Cursor to activate.\n'));
234
249
  } else {
235
250
  showError('Could not update Cursor config automatically.');
@@ -253,7 +268,7 @@ async function runSetup() {
253
268
  } else {
254
269
  showError('Could not run claude CLI automatically.');
255
270
  console.log(chalk.dim(' Run this command manually:\n'));
256
- console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
271
+ console.log(chalk.white(' claude mcp add --scope user circuit -- npx circuit-mcp\n'));
257
272
  }
258
273
  }
259
274
 
@@ -264,7 +279,7 @@ async function runSetup() {
264
279
  console.log(chalk.white(' { "mcpServers": { "circuit": { "command": "npx", "args": ["circuit-mcp"] } } }\n'));
265
280
  console.log(chalk.cyan.bold(' Claude Code'));
266
281
  console.log(chalk.dim(' Run:'));
267
- console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
282
+ console.log(chalk.white(' claude mcp add --scope user circuit -- npx circuit-mcp\n'));
268
283
  return;
269
284
  }
270
285
 
@@ -274,6 +289,8 @@ async function runSetup() {
274
289
 
275
290
  const authChoice = await prompt(chalk.cyan(' Sign in now? (Y/n): '));
276
291
 
292
+ let authSucceeded = false;
293
+
277
294
  if (authChoice.toLowerCase() !== 'n') {
278
295
  console.log();
279
296
  console.log(chalk.dim(' Opening browser to sign in...\n'));
@@ -281,6 +298,7 @@ async function runSetup() {
281
298
  try {
282
299
  const token = await authenticate();
283
300
  showSuccess('Signed in to Circuit');
301
+ authSucceeded = true;
284
302
  console.log();
285
303
  } catch (err) {
286
304
  showError('Could not sign in automatically.');
@@ -289,9 +307,17 @@ async function runSetup() {
289
307
  }
290
308
 
291
309
  console.log(chalk.dim(' ─────────────────────────────────────────\n'));
292
- console.log(chalk.white.bold(' You\'re all set!\n'));
293
- console.log(chalk.dim(' Restart your editor and start using Circuit.\n'));
294
- showUsageGuide();
310
+
311
+ if (authSucceeded || authChoice.toLowerCase() === 'n') {
312
+ console.log(chalk.white.bold(' You\'re all set!\n'));
313
+ console.log(chalk.dim(' Restart your editor and start using Circuit.\n'));
314
+ showUsageGuide();
315
+ } else {
316
+ console.log(chalk.white.bold(' Almost ready!\n'));
317
+ console.log(chalk.dim(' Sign in to finish connecting your Circuit account:\n'));
318
+ console.log(chalk.white(' npx circuit-mcp auth\n'));
319
+ console.log(chalk.dim(' Then restart your editor.\n'));
320
+ }
295
321
  }
296
322
 
297
323
  async function runAuth() {
@@ -325,6 +351,14 @@ async function runServer() {
325
351
  let token = await getStoredToken();
326
352
 
327
353
  if (!token) {
354
+ // Headless (spawned by Claude Code / Cursor over stdio): the browser auth
355
+ // flow has no one to drive it, so fail fast with instructions instead of
356
+ // hanging the MCP handshake.
357
+ if (!process.stdin.isTTY) {
358
+ console.error('Circuit MCP: not signed in. Run `npx circuit-mcp auth` in a terminal, then reconnect the MCP server.');
359
+ process.exit(1);
360
+ }
361
+
328
362
  // First run - show banner and auth
329
363
  showBanner();
330
364
  console.log(chalk.dim(' First time setup - let\'s connect your account.\n'));
package/src/pages.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Local auth result pages, served from 127.0.0.1 after the OAuth callback.
3
+ *
4
+ * Design: docs/superpowers/specs/2026-06-11-mcp-auth-pages-design.md
5
+ * Mirrors the product's /welcome onboarding cinematic system. Art and Geist
6
+ * load from app.withcircuit.com (CORS-open); offline the page degrades to a
7
+ * plain #1A1310 canvas with system fonts — values are hardcoded here because
8
+ * circuit-tokens.css is not available to the CLI.
9
+ */
10
+
11
+ const ASSET_ORIGIN = 'https://app.withcircuit.com';
12
+
13
+ const FAVICON = `data:image/svg+xml,<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'><defs><mask id='m'><circle cx='16' cy='16' r='13' fill='white'/><circle cx='16' cy='16' r='6.9' fill='black'/></mask></defs><ellipse cx='16' cy='17.73' rx='13' ry='11.27' fill='rgba(168,102,122,0.2)'/><circle cx='16' cy='16' r='13' fill='%23A8667A' mask='url(%23m)'/></svg>`;
14
+
15
+ const RING_MARK = `<svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden="true"><defs><mask id="ring"><circle cx="16" cy="16" r="13" fill="white"/><circle cx="16" cy="16" r="6.9" fill="black"/></mask></defs><circle cx="16" cy="16" r="13" fill="#A8667A" mask="url(#ring)"/></svg>`;
16
+
17
+ const COPY_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
18
+
19
+ const CHECK_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>`;
20
+
21
+ // Shared frame: backdrop layers + header + centered card.
22
+ // All values measured from the live /welcome flow (see spec table).
23
+ function renderPage({ title, eyebrow, eyebrowError, heading, body, pillHtml, hintHtml }) {
24
+ return `<!DOCTYPE html>
25
+ <html>
26
+ <head>
27
+ <meta charset="utf-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1">
29
+ <title>${title}</title>
30
+ <link rel="icon" type="image/svg+xml" href="${FAVICON}">
31
+ <style>
32
+ @font-face { font-family: 'Geist'; src: url('${ASSET_ORIGIN}/fonts/Geist-Variable.woff2') format('woff2'); font-weight: 100 900; font-display: swap; }
33
+ @font-face { font-family: 'Geist Mono'; src: url('${ASSET_ORIGIN}/fonts/GeistMono-Variable.woff2') format('woff2'); font-weight: 100 900; font-display: swap; }
34
+ * { margin: 0; padding: 0; box-sizing: border-box; }
35
+ body { font-family: 'Geist', -apple-system, system-ui, sans-serif; background: #1a1310; min-height: 100vh; }
36
+ .scene { position: relative; min-height: 100vh; overflow: hidden; }
37
+ .scene__bg { position: absolute; inset: 0; background-image: url('${ASSET_ORIGIN}/assets/ring-hero.png'); background-size: cover; background-position: 0% 38%; }
38
+ .scene__overlay { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(14,9,7,0.72), rgba(14,9,7,0.6), rgba(14,9,7,0.55)); }
39
+ .scene__grain { position: absolute; inset: 0; background: radial-gradient(rgba(244,228,201,0.5) 1px, transparent 1px); background-size: 3px 3px; opacity: 0.1; mix-blend-mode: overlay; }
40
+ .header { position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 16px; padding: 22px 28px; }
41
+ .header__wordmark { font-size: 17px; font-weight: 500; letter-spacing: -0.01em; color: #efe7da; }
42
+ .center { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
43
+ .card { background: rgba(20,15,13,0.78); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 16px; padding: 48px; width: 620px; }
44
+ .eyebrow { font-family: 'Geist Mono', ui-monospace, monospace; font-size: 11px; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: #c9919e; }
45
+ .eyebrow--error { color: #d06a55; }
46
+ h1 { font-size: 42px; font-weight: 500; letter-spacing: -0.03em; line-height: 1.08; color: #efe7da; margin-top: 24px; }
47
+ .body { font-size: 14.5px; line-height: 1.55; color: rgba(239,231,218,0.62); margin-top: 20px; max-width: 92%; }
48
+ .pill { display: flex; align-items: center; gap: 12px; margin-top: 26px; background: #0e0907; border-radius: 9px; padding: 13px 16px; font-family: 'Geist Mono', ui-monospace, monospace; font-size: 13px; color: #efe7da; box-shadow: inset 0 0 0 1px rgba(239,231,218,0.12); }
49
+ .pill__label { color: rgba(239,231,218,0.45); }
50
+ .pill__copy { margin-left: auto; display: inline-flex; align-items: center; background: none; border: none; padding: 4px; cursor: pointer; color: rgba(239,231,218,0.45); transition: color 200ms cubic-bezier(0, 0, 0.2, 1); /* --duration-normal --ease-out */ }
51
+ .pill__copy:hover { color: #efe7da; }
52
+ .hint { font-size: 12.5px; color: rgba(239,231,218,0.45); margin-top: 18px; }
53
+ @media (max-width: 899px) { .scene__bg { background-position: 50% 38%; } .card { width: 640px; max-width: calc(100% - 48px); } h1 { font-size: 40px; } }
54
+ @media (max-width: 639px) { .card { padding: 28px 24px; max-width: calc(100% - 24px); } h1 { font-size: 28px; } .body { max-width: 100%; } .pill { font-size: 12px; padding: 12px 14px; } }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div class="scene">
59
+ <div class="scene__bg"></div>
60
+ <div class="scene__overlay"></div>
61
+ <div class="scene__grain"></div>
62
+ <div class="header">${RING_MARK}<span class="header__wordmark">Circuit</span></div>
63
+ <div class="center">
64
+ <div class="card">
65
+ <p class="eyebrow${eyebrowError ? ' eyebrow--error' : ''}">${eyebrow}</p>
66
+ <h1>${heading}</h1>
67
+ <p class="body">${body}</p>
68
+ ${pillHtml}
69
+ ${hintHtml}
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <script>
74
+ document.querySelectorAll('[data-copy]').forEach((btn) => {
75
+ btn.addEventListener('click', async () => {
76
+ try {
77
+ await navigator.clipboard.writeText(btn.getAttribute('data-copy'));
78
+ btn.innerHTML = ${JSON.stringify(CHECK_ICON)};
79
+ setTimeout(() => { btn.innerHTML = ${JSON.stringify(COPY_ICON)}; }, 2000);
80
+ } catch {}
81
+ });
82
+ });
83
+ </script>
84
+ </body>
85
+ </html>`;
86
+ }
87
+
88
+ export function getSuccessPage() {
89
+ return renderPage({
90
+ title: 'Circuit — Connected',
91
+ eyebrow: 'Circuit MCP',
92
+ eyebrowError: false,
93
+ heading: 'Connected.',
94
+ body: 'Circuit is in your editor now. Your customers are about to start writing your next sprint.',
95
+ pillHtml: `<div class="pill"><span class="pill__label">Try:</span><span>&ldquo;What should I work on?&rdquo;</span><button type="button" class="pill__copy" aria-label="Copy prompt" data-copy="What should I work on?">${COPY_ICON}</button></div>`,
96
+ hintHtml: `<p class="hint">You can close this tab and return to your editor.</p>`,
97
+ });
98
+ }
99
+
100
+ export function getErrorPage(error) {
101
+ // Sanitize before interpolation — same contract as the old page
102
+ const safeError = String(error).replace(/[<>&"']/g, (c) => ({
103
+ '<': '&lt;',
104
+ '>': '&gt;',
105
+ '&': '&amp;',
106
+ '"': '&quot;',
107
+ "'": '&#39;',
108
+ })[c]);
109
+
110
+ return renderPage({
111
+ title: 'Circuit — Connection Failed',
112
+ eyebrow: 'Connection failed',
113
+ eyebrowError: true,
114
+ heading: 'Let&rsquo;s try that again.',
115
+ body: 'We couldn&rsquo;t finish signing you in. Run this in your terminal:',
116
+ pillHtml: `<div class="pill"><span>npx circuit-mcp auth</span><button type="button" class="pill__copy" aria-label="Copy command" data-copy="npx circuit-mcp auth">${COPY_ICON}</button></div>`,
117
+ hintHtml: `<p class="hint">${safeError}</p>`,
118
+ });
119
+ }
package/src/server.js CHANGED
@@ -84,7 +84,7 @@ async function handleMessage(message, token) {
84
84
  id,
85
85
  result: {
86
86
  protocolVersion: '2025-03-26',
87
- serverInfo: { name: 'circuit-mcp', version: '2.4.0' },
87
+ serverInfo: { name: 'circuit-mcp', version: '2.5.1' },
88
88
  capabilities: { tools: {}, resources: {} },
89
89
  instructions: `Circuit is connected. 4 tools available:
90
90
  • circuit.priorities — what to work on next