circuit-mcp 2.3.4 → 2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 Circuit (withcircuit.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "circuit-mcp",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
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
7
  "circuit-mcp": "./bin/circuit-mcp.js"
8
8
  },
9
9
  "main": "./src/index.js",
10
+ "exports": "./src/index.js",
11
+ "files": [
12
+ "bin/",
13
+ "src/",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
10
17
  "scripts": {
11
18
  "start": "node bin/circuit-mcp.js"
12
19
  },
@@ -25,15 +32,15 @@
25
32
  "customer-feedback"
26
33
  ],
27
34
  "author": "Circuit <hello@withcircuit.com>",
28
- "license": "UNLICENSED",
35
+ "license": "MIT",
29
36
  "homepage": "https://withcircuit.com",
30
37
  "repository": {
31
38
  "type": "git",
32
- "url": "https://github.com/CatherineWilliamsTreloar/Circuit.git",
39
+ "url": "https://github.com/withcircuit/Circuit.git",
33
40
  "directory": "circuit-mcp"
34
41
  },
35
42
  "bugs": {
36
- "url": "https://github.com/CatherineWilliamsTreloar/Circuit/issues"
43
+ "url": "https://github.com/withcircuit/Circuit/issues"
37
44
  },
38
45
  "dependencies": {
39
46
  "chalk": "^5.3.0",
package/src/auth.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import http from 'http';
2
2
  import { URL } from 'url';
3
- import { randomBytes } from 'crypto';
3
+ import { randomBytes, createHash } from 'crypto';
4
4
  import fs from 'fs/promises';
5
5
  import path from 'path';
6
6
  import os from 'os';
@@ -12,20 +12,53 @@ const CIRCUIT_URL = process.env.CIRCUIT_APP_URL || 'https://app.withcircuit.com'
12
12
  const TOKEN_FILE = path.join(os.homedir(), '.circuit', 'token.json');
13
13
 
14
14
  /**
15
- * Get stored token from disk
15
+ * Get stored token from disk. Attempts refresh if access token is expired.
16
16
  */
17
17
  export async function getStoredToken() {
18
18
  try {
19
19
  const data = await fs.readFile(TOKEN_FILE, 'utf-8');
20
- const { token, expiresAt } = JSON.parse(data);
20
+ const { token, expiresAt, refreshToken, refreshExpiresAt } = JSON.parse(data);
21
21
 
22
- // Check if expired
23
- if (expiresAt && new Date(expiresAt) < new Date()) {
24
- await clearToken();
25
- return null;
22
+ // Check if access token is still valid
23
+ if (expiresAt && new Date(expiresAt) > new Date()) {
24
+ return token;
26
25
  }
27
26
 
28
- return token;
27
+ // Access token expired — try refresh
28
+ if (refreshToken && refreshExpiresAt && new Date(refreshExpiresAt) > new Date()) {
29
+ const refreshed = await refreshAccessToken(refreshToken);
30
+ if (refreshed) return refreshed;
31
+ }
32
+
33
+ // Both expired
34
+ await clearToken();
35
+ return null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Refresh an expired access token using the refresh token.
43
+ */
44
+ async function refreshAccessToken(refreshToken) {
45
+ const CIRCUIT_API = process.env.CIRCUIT_API_URL || 'https://api.withcircuit.com';
46
+ try {
47
+ const response = await fetch(`${CIRCUIT_API}/mcp/token`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({
51
+ grant_type: 'refresh_token',
52
+ refresh_token: refreshToken,
53
+ }),
54
+ });
55
+ if (!response.ok) return null;
56
+ const data = await response.json();
57
+ if (data.access_token) {
58
+ await storeToken(data.access_token, data.expires_in, data.refresh_token);
59
+ return data.access_token;
60
+ }
61
+ return null;
29
62
  } catch {
30
63
  return null;
31
64
  }
@@ -34,12 +67,18 @@ export async function getStoredToken() {
34
67
  /**
35
68
  * Store token to disk
36
69
  */
37
- async function storeToken(token, expiresIn = 86400 * 30) {
70
+ async function storeToken(token, expiresIn = 86400 * 30, refreshToken = null) {
38
71
  const dir = path.dirname(TOKEN_FILE);
39
72
  await fs.mkdir(dir, { recursive: true });
40
73
 
41
74
  const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
42
- await fs.writeFile(TOKEN_FILE, JSON.stringify({ token, expiresAt }, null, 2));
75
+ const tokenData = { token, expiresAt };
76
+ if (refreshToken) {
77
+ tokenData.refreshToken = refreshToken;
78
+ // Refresh tokens last 90 days
79
+ tokenData.refreshExpiresAt = new Date(Date.now() + 90 * 86400 * 1000).toISOString();
80
+ }
81
+ await fs.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
43
82
  }
44
83
 
45
84
  /**
@@ -66,11 +105,18 @@ export async function authenticate() {
66
105
  const callbackUrl = `http://127.0.0.1:${port}/callback`;
67
106
  const state = randomBytes(16).toString('hex');
68
107
 
69
- // Build auth URL
108
+ // PKCE (RFC 7636): generate code_verifier and code_challenge
109
+ const codeVerifier = randomBytes(32).toString('base64url');
110
+ const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
111
+
112
+ // Build auth URL with PKCE (RFC 7636)
70
113
  const authUrl = new URL(`${CIRCUIT_URL}/mcp/auth`);
71
114
  authUrl.searchParams.set('redirect_uri', callbackUrl);
72
115
  authUrl.searchParams.set('state', state);
73
116
  authUrl.searchParams.set('response_type', 'code');
117
+ authUrl.searchParams.set('code_challenge', codeChallenge);
118
+ authUrl.searchParams.set('code_challenge_method', 'S256');
119
+ authUrl.searchParams.set('code_verifier', codeVerifier);
74
120
 
75
121
  // Handle callback
76
122
  server.on('request', async (req, res) => {
@@ -78,6 +124,7 @@ export async function authenticate() {
78
124
 
79
125
  if (url.pathname === '/callback') {
80
126
  const token = url.searchParams.get('access_token');
127
+ const refreshTokenParam = url.searchParams.get('refresh_token');
81
128
  const error = url.searchParams.get('error');
82
129
  const returnedState = url.searchParams.get('state');
83
130
 
@@ -97,8 +144,8 @@ export async function authenticate() {
97
144
  return;
98
145
  }
99
146
 
100
- // Store token
101
- await storeToken(token);
147
+ // Store token (with refresh token if available)
148
+ await storeToken(token, 86400 * 30, refreshTokenParam);
102
149
 
103
150
  // Send success page and close after response is fully sent
104
151
  res.writeHead(200, { 'Content-Type': 'text/html' });
package/src/server.js CHANGED
@@ -83,8 +83,8 @@ async function handleMessage(message, token) {
83
83
  jsonrpc: '2.0',
84
84
  id,
85
85
  result: {
86
- protocolVersion: '2024-11-05',
87
- serverInfo: { name: 'circuit-mcp', version: '2.2.0' },
86
+ protocolVersion: '2025-03-26',
87
+ serverInfo: { name: 'circuit-mcp', version: '2.4.0' },
88
88
  capabilities: { tools: {}, resources: {} },
89
89
  instructions: `Circuit is connected. 4 tools available:
90
90
  • circuit.priorities — what to work on next
@@ -201,14 +201,14 @@ const TOOLS = [
201
201
  },
202
202
  {
203
203
  name: 'circuit.act',
204
- description: "Take action. Start building, ship it, share back with customers, assign, correct a classification, submit feedback, or submit a transcript.",
204
+ description: "Take action. Start building, ship it, share back with customers, assign, correct a classification, park a priority, submit feedback, or submit a transcript.",
205
205
  inputSchema: {
206
206
  type: 'object',
207
207
  properties: {
208
208
  action: {
209
209
  type: 'string',
210
- description: "'build' (start building), 'ship' (mark shipped), 'share' (notify customers via email/widget), 'assign' (assign to team member), 'correct' (fix classification), 'submit' (add feedback), 'transcript' (submit a transcript)",
211
- enum: ['build', 'ship', 'share', 'assign', 'correct', 'submit', 'transcript']
210
+ description: "'build' (start building), 'ship' (mark shipped), 'share' (notify customers via email/widget), 'assign' (assign to team member), 'correct' (fix classification), 'park' (park priority with reason), 'submit' (add feedback), 'transcript' (submit a transcript)",
211
+ enum: ['build', 'ship', 'share', 'assign', 'correct', 'park', 'submit', 'transcript']
212
212
  },
213
213
  spec_id: {
214
214
  type: 'string',
@@ -229,7 +229,11 @@ const TOOLS = [
229
229
  },
230
230
  priority_id: {
231
231
  type: 'string',
232
- description: "Priority ID (for correct)"
232
+ description: "Priority ID (for correct, park)"
233
+ },
234
+ park_reason: {
235
+ type: 'string',
236
+ description: "Reason for parking this priority (for park action). E.g. 'Out of scope this quarter', 'Waiting for more signal', 'Low revenue impact'"
233
237
  },
234
238
  correction_type: {
235
239
  type: 'string',
@@ -557,11 +561,12 @@ Ranked customer priorities with revenue impact and trend data.
557
561
  * "Export all ready specs for sprint planning"
558
562
 
559
563
  ## circuit.act — Ship it and close the loop
560
- Start building, mark as shipped, notify customers, assign to a team member, or submit new feedback.
564
+ Start building, mark as shipped, notify customers, assign, park a priority, or submit new feedback.
561
565
  * "Start building <spec_id>"
562
566
  * "Mark <spec_id> as shipped"
563
567
  * "Tell customers we shipped <spec_id>"
564
568
  * "Assign <spec_id> to Catherine"
569
+ * "Park <priority_id> — waiting for more signal"
565
570
  * "Submit feedback: users want dark mode"
566
571
  * "Submit transcript: <transcript text>"
567
572
 
@@ -1,57 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Circuit - Connection Failed</title>
5
- <style>
6
- * { margin: 0; padding: 0; box-sizing: border-box; }
7
- body {
8
- font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
9
- background: #F5F3F0;
10
- min-height: 100vh;
11
- display: flex;
12
- align-items: center;
13
- justify-content: center;
14
- color: #1C1A18;
15
- padding: 24px;
16
- }
17
- .content {
18
- text-align: center;
19
- max-width: 400px;
20
- }
21
- .icon {
22
- margin-bottom: 24px;
23
- }
24
- .icon svg {
25
- width: 48px;
26
- height: 48px;
27
- color: #D64545;
28
- }
29
- h1 {
30
- font-size: 24px;
31
- font-weight: 600;
32
- color: #1C1A18;
33
- margin-bottom: 12px;
34
- }
35
- p {
36
- font-size: 14px;
37
- color: rgba(28, 26, 24, 0.6);
38
- line-height: 1.6;
39
- }
40
- .hint {
41
- font-size: 12px;
42
- color: rgba(28, 26, 24, 0.6);
43
- margin-top: 16px;
44
- }
45
- </style>
46
- </head>
47
- <body>
48
- <div class="content">
49
- <div class="icon">
50
- <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>
51
- </div>
52
- <h1>Connection Failed</h1>
53
- <p>Authentication timed out</p>
54
- <p class="hint">Please try connecting again from your AI assistant.</p>
55
- </div>
56
- </body>
57
- </html>
@@ -1,57 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Circuit - Connected</title>
5
- <style>
6
- * { margin: 0; padding: 0; box-sizing: border-box; }
7
- body {
8
- font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
9
- background: #F5F3F0;
10
- min-height: 100vh;
11
- display: flex;
12
- align-items: center;
13
- justify-content: center;
14
- color: #1C1A18;
15
- padding: 24px;
16
- }
17
- .content {
18
- text-align: center;
19
- max-width: 400px;
20
- }
21
- .icon {
22
- margin-bottom: 24px;
23
- }
24
- .icon svg {
25
- width: 48px;
26
- height: 48px;
27
- color: #1C1A18;
28
- }
29
- h1 {
30
- font-size: 24px;
31
- font-weight: 600;
32
- color: #1C1A18;
33
- margin-bottom: 12px;
34
- }
35
- p {
36
- font-size: 14px;
37
- color: rgba(28, 26, 24, 0.6);
38
- line-height: 1.6;
39
- }
40
- .hint {
41
- font-size: 12px;
42
- color: rgba(28, 26, 24, 0.6);
43
- margin-top: 16px;
44
- }
45
- </style>
46
- </head>
47
- <body>
48
- <div class="content">
49
- <div class="icon">
50
- <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>
51
- </div>
52
- <h1>Connected.</h1>
53
- <p>Your AI coding assistant is now connected to Circuit.</p>
54
- <p class="hint">This window will close automatically...</p>
55
- </div>
56
- </body>
57
- </html>
package/publish.sh DELETED
@@ -1,30 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Publish script for circuit-mcp package
4
- # Uses token to bypass 2FA
5
- #
6
- # Usage:
7
- # NPM_TOKEN=your_token ./publish.sh
8
- #
9
- # Or set NPM_TOKEN in your environment
10
-
11
- set -e
12
-
13
- # Use NPM_TOKEN from environment, or prompt if not set
14
- if [ -z "$NPM_TOKEN" ]; then
15
- echo "Error: NPM_TOKEN environment variable is required"
16
- echo "Usage: NPM_TOKEN=your_token ./publish.sh"
17
- exit 1
18
- fi
19
-
20
- # Create temporary .npmrc with token
21
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
22
-
23
- # Publish the package
24
- npm publish --access public
25
-
26
- # Clean up .npmrc
27
- rm .npmrc
28
-
29
- echo "✅ Published successfully!"
30
-