circuit-mcp 1.0.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 +82 -0
- package/bin/circuit-mcp.js +8 -0
- package/package.json +30 -0
- package/src/auth.js +217 -0
- package/src/index.js +108 -0
- package/src/server.js +286 -0
- package/src/ui.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Circuit MCP
|
|
2
|
+
|
|
3
|
+
Connect [Circuit](https://withcircuit.com) to Cursor and Claude Code via MCP (Model Context Protocol).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Cursor
|
|
8
|
+
|
|
9
|
+
Add to your Cursor settings (`Cmd+Shift+P` → "Cursor Settings: Open User Settings"):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"circuit": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["circuit-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Claude Code
|
|
23
|
+
|
|
24
|
+
Run this command:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
claude mcp add circuit -- npx circuit-mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## First Run
|
|
31
|
+
|
|
32
|
+
On first use, Circuit will open your browser to authenticate. After signing in, your token is cached locally at `~/.circuit/token.json`.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
╭──────────────────────────────────╮
|
|
36
|
+
│ ⚡ Circuit MCP │
|
|
37
|
+
╰──────────────────────────────────╯
|
|
38
|
+
|
|
39
|
+
First time setup - let's connect your account.
|
|
40
|
+
|
|
41
|
+
Opening browser to authenticate...
|
|
42
|
+
|
|
43
|
+
✓ Connected!
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Start MCP server (used by Cursor/Claude)
|
|
50
|
+
npx circuit-mcp
|
|
51
|
+
|
|
52
|
+
# Show setup instructions
|
|
53
|
+
npx circuit-mcp setup
|
|
54
|
+
|
|
55
|
+
# Re-authenticate
|
|
56
|
+
npx circuit-mcp auth
|
|
57
|
+
|
|
58
|
+
# Log out (clear stored token)
|
|
59
|
+
npx circuit-mcp logout
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Available Tools
|
|
63
|
+
|
|
64
|
+
Once connected, your AI coding assistant can use these tools:
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `get_priorities` | Get top customer feedback priorities |
|
|
69
|
+
| `get_brief` | Get the engineering brief for a priority |
|
|
70
|
+
| `get_feedback` | Get raw customer feedback items |
|
|
71
|
+
|
|
72
|
+
### Example Usage in Cursor/Claude
|
|
73
|
+
|
|
74
|
+
> "What are the top 5 customer priorities?"
|
|
75
|
+
|
|
76
|
+
> "Get the brief for priority #1 and help me implement it"
|
|
77
|
+
|
|
78
|
+
> "Show me recent feedback about authentication issues"
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "circuit-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Circuit MCP server for Cursor and Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"circuit-mcp": "./bin/circuit-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/circuit-mcp.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"circuit",
|
|
15
|
+
"mcp",
|
|
16
|
+
"cursor",
|
|
17
|
+
"claude",
|
|
18
|
+
"ai",
|
|
19
|
+
"feedback"
|
|
20
|
+
],
|
|
21
|
+
"author": "Circuit",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"open": "^10.1.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { showSpinner, showInfo, showPrompt } from './ui.js';
|
|
10
|
+
|
|
11
|
+
const CIRCUIT_URL = 'https://app.withcircuit.com';
|
|
12
|
+
const TOKEN_FILE = path.join(os.homedir(), '.circuit', 'token.json');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get stored token from disk
|
|
16
|
+
*/
|
|
17
|
+
export async function getStoredToken() {
|
|
18
|
+
try {
|
|
19
|
+
const data = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
20
|
+
const { token, expiresAt } = JSON.parse(data);
|
|
21
|
+
|
|
22
|
+
// Check if expired
|
|
23
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
24
|
+
await clearToken();
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return token;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Store token to disk
|
|
36
|
+
*/
|
|
37
|
+
async function storeToken(token, expiresIn = 86400 * 30) {
|
|
38
|
+
const dir = path.dirname(TOKEN_FILE);
|
|
39
|
+
await fs.mkdir(dir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
42
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify({ token, expiresAt }, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clear stored token
|
|
47
|
+
*/
|
|
48
|
+
export async function clearToken() {
|
|
49
|
+
try {
|
|
50
|
+
await fs.unlink(TOKEN_FILE);
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore if file doesn't exist
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Authenticate with Circuit via OAuth
|
|
58
|
+
*/
|
|
59
|
+
export async function authenticate() {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
// Find available port
|
|
62
|
+
const server = http.createServer();
|
|
63
|
+
|
|
64
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
65
|
+
const { port } = server.address();
|
|
66
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
67
|
+
const state = randomBytes(16).toString('hex');
|
|
68
|
+
|
|
69
|
+
// Build auth URL
|
|
70
|
+
const authUrl = new URL(`${CIRCUIT_URL}/mcp/auth`);
|
|
71
|
+
authUrl.searchParams.set('redirect_uri', callbackUrl);
|
|
72
|
+
authUrl.searchParams.set('state', state);
|
|
73
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
74
|
+
|
|
75
|
+
// Handle callback
|
|
76
|
+
server.on('request', async (req, res) => {
|
|
77
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
78
|
+
|
|
79
|
+
if (url.pathname === '/callback') {
|
|
80
|
+
const token = url.searchParams.get('access_token');
|
|
81
|
+
const error = url.searchParams.get('error');
|
|
82
|
+
const returnedState = url.searchParams.get('state');
|
|
83
|
+
|
|
84
|
+
if (error) {
|
|
85
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
86
|
+
res.end(getErrorPage(error));
|
|
87
|
+
server.close();
|
|
88
|
+
reject(new Error(error));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!token) {
|
|
93
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
94
|
+
res.end(getErrorPage('No token received'));
|
|
95
|
+
server.close();
|
|
96
|
+
reject(new Error('No token received'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Store token
|
|
101
|
+
await storeToken(token);
|
|
102
|
+
|
|
103
|
+
// Send success page
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
105
|
+
res.end(getSuccessPage());
|
|
106
|
+
|
|
107
|
+
server.close();
|
|
108
|
+
resolve(token);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Open browser
|
|
113
|
+
console.log(chalk.dim(' Opening browser to authenticate...\n'));
|
|
114
|
+
showInfo(`If browser doesn't open, visit:\n ${chalk.cyan(authUrl.toString())}\n`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await open(authUrl.toString());
|
|
118
|
+
} catch {
|
|
119
|
+
// Browser failed to open, user will need to click link
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Timeout after 5 minutes
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
server.close();
|
|
125
|
+
reject(new Error('Authentication timed out'));
|
|
126
|
+
}, 5 * 60 * 1000);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.on('error', reject);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getSuccessPage() {
|
|
134
|
+
return `<!DOCTYPE html>
|
|
135
|
+
<html>
|
|
136
|
+
<head>
|
|
137
|
+
<title>Circuit - Connected!</title>
|
|
138
|
+
<style>
|
|
139
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
140
|
+
body {
|
|
141
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
142
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
143
|
+
min-height: 100vh;
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
color: white;
|
|
148
|
+
}
|
|
149
|
+
.container {
|
|
150
|
+
text-align: center;
|
|
151
|
+
padding: 48px;
|
|
152
|
+
}
|
|
153
|
+
.icon {
|
|
154
|
+
font-size: 64px;
|
|
155
|
+
margin-bottom: 24px;
|
|
156
|
+
}
|
|
157
|
+
h1 {
|
|
158
|
+
font-size: 32px;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
margin-bottom: 16px;
|
|
161
|
+
color: #6366F1;
|
|
162
|
+
}
|
|
163
|
+
p {
|
|
164
|
+
font-size: 18px;
|
|
165
|
+
color: rgba(255,255,255,0.7);
|
|
166
|
+
margin-bottom: 8px;
|
|
167
|
+
}
|
|
168
|
+
.hint {
|
|
169
|
+
margin-top: 32px;
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
color: rgba(255,255,255,0.5);
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<div class="container">
|
|
177
|
+
<div class="icon">⚡</div>
|
|
178
|
+
<h1>Connected to Circuit!</h1>
|
|
179
|
+
<p>You can close this window and return to your terminal.</p>
|
|
180
|
+
<p class="hint">Circuit is now ready to use in Cursor or Claude Code.</p>
|
|
181
|
+
</div>
|
|
182
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
183
|
+
</body>
|
|
184
|
+
</html>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getErrorPage(error) {
|
|
188
|
+
return `<!DOCTYPE html>
|
|
189
|
+
<html>
|
|
190
|
+
<head>
|
|
191
|
+
<title>Circuit - Error</title>
|
|
192
|
+
<style>
|
|
193
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
194
|
+
body {
|
|
195
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
196
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
197
|
+
min-height: 100vh;
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
justify-content: center;
|
|
201
|
+
color: white;
|
|
202
|
+
}
|
|
203
|
+
.container { text-align: center; padding: 48px; }
|
|
204
|
+
.icon { font-size: 64px; margin-bottom: 24px; }
|
|
205
|
+
h1 { font-size: 32px; color: #ef4444; margin-bottom: 16px; }
|
|
206
|
+
p { font-size: 18px; color: rgba(255,255,255,0.7); }
|
|
207
|
+
</style>
|
|
208
|
+
</head>
|
|
209
|
+
<body>
|
|
210
|
+
<div class="container">
|
|
211
|
+
<div class="icon">⚠️</div>
|
|
212
|
+
<h1>Authentication Failed</h1>
|
|
213
|
+
<p>${error}</p>
|
|
214
|
+
</div>
|
|
215
|
+
</body>
|
|
216
|
+
</html>`;
|
|
217
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { authenticate, getStoredToken } from './auth.js';
|
|
3
|
+
import { startMcpServer } from './server.js';
|
|
4
|
+
import { showBanner, showSuccess, showError, showSpinner } from './ui.js';
|
|
5
|
+
|
|
6
|
+
const CIRCUIT_URL = 'https://app.withcircuit.com';
|
|
7
|
+
|
|
8
|
+
export async function main() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
// Handle setup command
|
|
12
|
+
if (args[0] === 'setup') {
|
|
13
|
+
await runSetup();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Handle auth command
|
|
18
|
+
if (args[0] === 'auth') {
|
|
19
|
+
await runAuth();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle logout command
|
|
24
|
+
if (args[0] === 'logout') {
|
|
25
|
+
await runLogout();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Default: start MCP server
|
|
30
|
+
await runServer();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runSetup() {
|
|
34
|
+
showBanner();
|
|
35
|
+
|
|
36
|
+
console.log(chalk.dim(' Choose your AI coding tool:\n'));
|
|
37
|
+
console.log(` ${chalk.bold('1.')} Cursor`);
|
|
38
|
+
console.log(` ${chalk.bold('2.')} Claude Code`);
|
|
39
|
+
console.log(` ${chalk.bold('3.')} Both\n`);
|
|
40
|
+
|
|
41
|
+
// For now, show instructions for both
|
|
42
|
+
console.log(chalk.cyan.bold(' Cursor Setup\n'));
|
|
43
|
+
console.log(chalk.dim(' Add to your Cursor settings (Cmd+Shift+P → "Cursor Settings: Open User Settings"):\n'));
|
|
44
|
+
console.log(chalk.white(` {
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"circuit": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["circuit-mcp"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}\n`));
|
|
52
|
+
|
|
53
|
+
console.log(chalk.cyan.bold(' Claude Code Setup\n'));
|
|
54
|
+
console.log(chalk.dim(' Run this command in your terminal:\n'));
|
|
55
|
+
console.log(chalk.white(` claude mcp add circuit -- npx circuit-mcp\n`));
|
|
56
|
+
|
|
57
|
+
console.log(chalk.dim(' ─────────────────────────────────────────\n'));
|
|
58
|
+
console.log(chalk.dim(' After setup, Circuit will prompt you to'));
|
|
59
|
+
console.log(chalk.dim(' authenticate on first use.\n'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runAuth() {
|
|
63
|
+
showBanner();
|
|
64
|
+
|
|
65
|
+
const spinner = showSpinner('Authenticating...');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const token = await authenticate();
|
|
69
|
+
spinner.stop();
|
|
70
|
+
showSuccess('Authenticated successfully!');
|
|
71
|
+
console.log(chalk.dim('\n Your token is stored locally.\n'));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
spinner.stop();
|
|
74
|
+
showError(`Authentication failed: ${err.message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runLogout() {
|
|
80
|
+
showBanner();
|
|
81
|
+
|
|
82
|
+
const { clearToken } = await import('./auth.js');
|
|
83
|
+
await clearToken();
|
|
84
|
+
showSuccess('Logged out successfully.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function runServer() {
|
|
88
|
+
// Check for existing token
|
|
89
|
+
let token = await getStoredToken();
|
|
90
|
+
|
|
91
|
+
if (!token) {
|
|
92
|
+
// First run - show banner and auth
|
|
93
|
+
showBanner();
|
|
94
|
+
console.log(chalk.dim(' First time setup - let\'s connect your account.\n'));
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
token = await authenticate();
|
|
98
|
+
showSuccess('Connected!');
|
|
99
|
+
console.log();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
showError(`Authentication failed: ${err.message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Start MCP server (stdio mode for Cursor/Claude)
|
|
107
|
+
await startMcpServer(token);
|
|
108
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
|
|
3
|
+
const CIRCUIT_API = 'https://app.withcircuit.com';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Start MCP server in stdio mode
|
|
7
|
+
* Communicates with Cursor/Claude via JSON-RPC over stdin/stdout
|
|
8
|
+
*/
|
|
9
|
+
export async function startMcpServer(token) {
|
|
10
|
+
const rl = createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
terminal: false
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Handle incoming JSON-RPC messages
|
|
17
|
+
rl.on('line', async (line) => {
|
|
18
|
+
try {
|
|
19
|
+
const message = JSON.parse(line);
|
|
20
|
+
const response = await handleMessage(message, token);
|
|
21
|
+
if (response) {
|
|
22
|
+
console.log(JSON.stringify(response));
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// Send error response
|
|
26
|
+
console.log(JSON.stringify({
|
|
27
|
+
jsonrpc: '2.0',
|
|
28
|
+
error: { code: -32700, message: 'Parse error' },
|
|
29
|
+
id: null
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Keep process alive
|
|
35
|
+
process.stdin.resume();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handle incoming MCP message
|
|
40
|
+
*/
|
|
41
|
+
async function handleMessage(message, token) {
|
|
42
|
+
const { id, method, params } = message;
|
|
43
|
+
|
|
44
|
+
switch (method) {
|
|
45
|
+
case 'initialize':
|
|
46
|
+
return {
|
|
47
|
+
jsonrpc: '2.0',
|
|
48
|
+
id,
|
|
49
|
+
result: {
|
|
50
|
+
protocolVersion: '2024-11-05',
|
|
51
|
+
serverInfo: {
|
|
52
|
+
name: 'circuit-mcp',
|
|
53
|
+
version: '1.0.0'
|
|
54
|
+
},
|
|
55
|
+
capabilities: {
|
|
56
|
+
tools: {},
|
|
57
|
+
resources: {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
case 'initialized':
|
|
63
|
+
// No response needed for notification
|
|
64
|
+
return null;
|
|
65
|
+
|
|
66
|
+
case 'tools/list':
|
|
67
|
+
return {
|
|
68
|
+
jsonrpc: '2.0',
|
|
69
|
+
id,
|
|
70
|
+
result: {
|
|
71
|
+
tools: [
|
|
72
|
+
{
|
|
73
|
+
name: 'get_priorities',
|
|
74
|
+
description: 'Get the top customer feedback priorities from Circuit',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
limit: {
|
|
79
|
+
type: 'number',
|
|
80
|
+
description: 'Maximum number of priorities to return (default: 10)'
|
|
81
|
+
},
|
|
82
|
+
focus: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Focus type: volume, revenue, urgency, negative, positive, feature',
|
|
85
|
+
enum: ['volume', 'revenue', 'urgency', 'negative', 'positive', 'feature']
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'get_brief',
|
|
92
|
+
description: 'Get the engineering brief for a specific priority',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
priority_id: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'The ID of the priority to get the brief for'
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
required: ['priority_id']
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'get_feedback',
|
|
106
|
+
description: 'Get raw customer feedback items',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
limit: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'Maximum number of feedback items to return (default: 20)'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
case 'tools/call':
|
|
122
|
+
return await handleToolCall(id, params, token);
|
|
123
|
+
|
|
124
|
+
case 'resources/list':
|
|
125
|
+
return {
|
|
126
|
+
jsonrpc: '2.0',
|
|
127
|
+
id,
|
|
128
|
+
result: { resources: [] }
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
case 'ping':
|
|
132
|
+
return {
|
|
133
|
+
jsonrpc: '2.0',
|
|
134
|
+
id,
|
|
135
|
+
result: {}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
default:
|
|
139
|
+
return {
|
|
140
|
+
jsonrpc: '2.0',
|
|
141
|
+
id,
|
|
142
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Handle tool calls
|
|
149
|
+
*/
|
|
150
|
+
async function handleToolCall(id, params, token) {
|
|
151
|
+
const { name, arguments: args } = params;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
let result;
|
|
155
|
+
|
|
156
|
+
switch (name) {
|
|
157
|
+
case 'get_priorities':
|
|
158
|
+
result = await fetchPriorities(token, args);
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case 'get_brief':
|
|
162
|
+
result = await fetchBrief(token, args);
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case 'get_feedback':
|
|
166
|
+
result = await fetchFeedback(token, args);
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
return {
|
|
171
|
+
jsonrpc: '2.0',
|
|
172
|
+
id,
|
|
173
|
+
error: { code: -32602, message: `Unknown tool: ${name}` }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
jsonrpc: '2.0',
|
|
179
|
+
id,
|
|
180
|
+
result: {
|
|
181
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
jsonrpc: '2.0',
|
|
188
|
+
id,
|
|
189
|
+
error: { code: -32000, message: err.message }
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fetch priorities from Circuit API
|
|
196
|
+
*/
|
|
197
|
+
async function fetchPriorities(token, args = {}) {
|
|
198
|
+
const limit = args.limit || 10;
|
|
199
|
+
const focus = args.focus || 'volume';
|
|
200
|
+
|
|
201
|
+
const url = `${CIRCUIT_API}/api/focus?limit=${limit}&focus_type=${focus}`;
|
|
202
|
+
|
|
203
|
+
const response = await fetch(url, {
|
|
204
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
throw new Error(`API error: ${response.status}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = await response.json();
|
|
212
|
+
|
|
213
|
+
// Format for readability
|
|
214
|
+
return (data.priorities || data.clusters || []).map((p, i) => ({
|
|
215
|
+
rank: i + 1,
|
|
216
|
+
id: p.cluster_id || p.id,
|
|
217
|
+
title: p.summary || p.theme,
|
|
218
|
+
category: p.category,
|
|
219
|
+
mentions: p.volume || p.count,
|
|
220
|
+
urgency: p.urgency_score,
|
|
221
|
+
trend: p.trend
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Fetch a specific brief from Circuit API
|
|
227
|
+
*/
|
|
228
|
+
async function fetchBrief(token, args) {
|
|
229
|
+
if (!args.priority_id) {
|
|
230
|
+
throw new Error('priority_id is required');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const url = `${CIRCUIT_API}/api/builds?cluster_id=${args.priority_id}`;
|
|
234
|
+
|
|
235
|
+
const response = await fetch(url, {
|
|
236
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error(`API error: ${response.status}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const data = await response.json();
|
|
244
|
+
const build = (data.builds || []).find(b =>
|
|
245
|
+
b.cluster_id === args.priority_id || b.clusterId === args.priority_id
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (!build) {
|
|
249
|
+
throw new Error('Brief not found for this priority');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
id: build.id,
|
|
254
|
+
title: build.title,
|
|
255
|
+
status: build.status,
|
|
256
|
+
content: build.build || build.spec_content
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Fetch raw feedback from Circuit API
|
|
262
|
+
*/
|
|
263
|
+
async function fetchFeedback(token, args = {}) {
|
|
264
|
+
const limit = args.limit || 20;
|
|
265
|
+
|
|
266
|
+
const url = `${CIRCUIT_API}/api/feedback/all?limit=${limit}`;
|
|
267
|
+
|
|
268
|
+
const response = await fetch(url, {
|
|
269
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
throw new Error(`API error: ${response.status}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const data = await response.json();
|
|
277
|
+
|
|
278
|
+
return (data.feedback || []).map(f => ({
|
|
279
|
+
id: f.id,
|
|
280
|
+
text: f.text,
|
|
281
|
+
source: f.source,
|
|
282
|
+
sentiment: f.analysis?.sentiment,
|
|
283
|
+
intent: f.analysis?.intent,
|
|
284
|
+
created_at: f.created_at
|
|
285
|
+
}));
|
|
286
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const CIRCUIT_PURPLE = '#6366F1';
|
|
4
|
+
|
|
5
|
+
export function showBanner() {
|
|
6
|
+
console.log();
|
|
7
|
+
console.log(chalk.hex(CIRCUIT_PURPLE).bold(' ╭──────────────────────────────────╮'));
|
|
8
|
+
console.log(chalk.hex(CIRCUIT_PURPLE).bold(' │') + chalk.white.bold(' ⚡ Circuit MCP ') + chalk.hex(CIRCUIT_PURPLE).bold('│'));
|
|
9
|
+
console.log(chalk.hex(CIRCUIT_PURPLE).bold(' ╰──────────────────────────────────╯'));
|
|
10
|
+
console.log();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function showSuccess(message) {
|
|
14
|
+
console.log(chalk.green(' ✓ ') + chalk.white(message));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function showError(message) {
|
|
18
|
+
console.log(chalk.red(' ✗ ') + chalk.white(message));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function showInfo(message) {
|
|
22
|
+
console.log(chalk.blue(' ℹ ') + chalk.dim(message));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function showStep(step, message) {
|
|
26
|
+
console.log(chalk.dim(` ${step}. `) + chalk.white(message));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function showSpinner(message) {
|
|
30
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
31
|
+
let i = 0;
|
|
32
|
+
let stopped = false;
|
|
33
|
+
|
|
34
|
+
const interval = setInterval(() => {
|
|
35
|
+
if (stopped) return;
|
|
36
|
+
process.stderr.write(`\r ${chalk.hex(CIRCUIT_PURPLE)(frames[i])} ${chalk.dim(message)}`);
|
|
37
|
+
i = (i + 1) % frames.length;
|
|
38
|
+
}, 80);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
stop: () => {
|
|
42
|
+
stopped = true;
|
|
43
|
+
clearInterval(interval);
|
|
44
|
+
process.stderr.write('\r' + ' '.repeat(message.length + 10) + '\r');
|
|
45
|
+
},
|
|
46
|
+
update: (newMessage) => {
|
|
47
|
+
message = newMessage;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function showPrompt(message) {
|
|
53
|
+
process.stdout.write(chalk.cyan(' → ') + chalk.white(message));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function clearLine() {
|
|
57
|
+
process.stdout.write('\r\x1b[K');
|
|
58
|
+
}
|