circuit-mcp 2.0.0 → 2.3.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 +132 -53
- package/package.json +1 -1
- package/preview-error.html +57 -0
- package/preview-success.html +57 -0
- package/src/auth.js +74 -63
- package/src/server.js +348 -263
package/README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# circuit-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Customer feedback in. Engineering specs out. Solve it with your AI coding tool.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Connect [Circuit](https://withcircuit.com) to **Cursor** and **Claude Code** via the Model Context Protocol.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### Claude Code
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add circuit -- npx circuit-mcp
|
|
15
|
+
```
|
|
8
16
|
|
|
9
17
|
### Cursor
|
|
10
18
|
|
|
@@ -21,77 +29,148 @@ Add to `~/.cursor/mcp.json`:
|
|
|
21
29
|
}
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
First run opens your browser to authenticate. Token is cached at `~/.circuit/token.json` (30-day expiry).
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
claude mcp add circuit -- npx circuit-mcp
|
|
28
|
-
```
|
|
34
|
+
---
|
|
29
35
|
|
|
30
|
-
##
|
|
36
|
+
## 4 Tools
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
### `circuit.priorities`
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
Circuit MCP
|
|
40
|
+
What should I work on? Ranked customer priorities with trends, confidence, and pattern matching.
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
| Parameter | Type | Description |
|
|
43
|
+
|-----------|------|-------------|
|
|
44
|
+
| `weekly` | boolean | Set to `true` to get the weekly digest instead (movements, new entries, drops, spikes) |
|
|
45
|
+
| `lens` | string | How to rank: `volume`, `urgency`, `revenue`, `retention`, `delight`, `feature` |
|
|
46
|
+
| `segment` | string | Customer segment: `enterprise`, `smb`, `all` |
|
|
47
|
+
| `limit` | number | Number of results (default: 5, max: 20) |
|
|
48
|
+
| `category` | string | Filter: `Bug`, `Feature`, `Friction`, `Complaint`, `Praise` |
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
**Also returns:** Session context (last ship, in-progress builds, suggested next action) and instinct confidence (pattern matching from your shipping history).
|
|
51
|
+
|
|
52
|
+
**Weekly mode** (`weekly: true`): Returns priority movements (rank changes), new entries, dropped items, and volume spikes compared to last week.
|
|
53
|
+
|
|
54
|
+
### `circuit.spec`
|
|
55
|
+
|
|
56
|
+
Full engineering spec for a priority. 5 sections: What to Build, Why It Matters, Customer Voice (verbatim quotes), Files to Touch, Done When.
|
|
57
|
+
|
|
58
|
+
| Parameter | Type | Description |
|
|
59
|
+
|-----------|------|-------------|
|
|
60
|
+
| `priority_id` | string | Priority ID from `circuit.priorities` |
|
|
61
|
+
| `spec_id` | string | Spec ID directly (alternative to priority_id) |
|
|
62
|
+
| `include_history` | boolean | Include version history and ship memory (default: true) |
|
|
63
|
+
| `batch` | boolean | Set to `true` to export multiple specs as markdown |
|
|
64
|
+
| `spec_ids` | string[] | Specific spec IDs to export (batch mode) |
|
|
65
|
+
| `status` | string | Filter specs by status in batch mode: `ready`, `building`, `shipped` |
|
|
66
|
+
| `limit` | number | Number of specs in batch mode (default: 10, max: 50) |
|
|
67
|
+
|
|
68
|
+
**Also returns:** Post-ship signal (new feedback since shipping), effort estimate, codebase context (tech stack, related PRs, AI config files like CLAUDE.md), and related memories from previous ships.
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
**Batch mode** (`batch: true`): Exports multiple specs as formatted markdown for sprint planning or documentation.
|
|
44
71
|
|
|
45
|
-
|
|
46
|
-
|------|-------------|
|
|
47
|
-
| `get_priorities` | Get top customer priorities ranked by volume, urgency, or sentiment |
|
|
48
|
-
| `get_brief` | Get the engineering spec for a priority (what to build, why, done criteria) |
|
|
49
|
-
| `search_feedback` | Search raw customer feedback by keyword |
|
|
50
|
-
| `start_building` | Mark a brief as "building" - you're working on it |
|
|
51
|
-
| `mark_done` | Mark a brief as "done" - it shipped! |
|
|
72
|
+
### `circuit.act`
|
|
52
73
|
|
|
53
|
-
|
|
74
|
+
Take action. Start building, ship it, share back, assign, correct, submit feedback, or submit a transcript.
|
|
54
75
|
|
|
55
|
-
|
|
76
|
+
| Action | What it does |
|
|
77
|
+
|--------|-------------|
|
|
78
|
+
| `build` | Mark spec as "building" |
|
|
79
|
+
| `ship` | Mark as shipped, record memory |
|
|
80
|
+
| `share` | Notify customers who submitted feedback (email, widget, or both) |
|
|
81
|
+
| `assign` | Assign spec to a team member by email or user ID |
|
|
82
|
+
| `correct` | Fix a priority's category classification (Circuit remembers corrections) |
|
|
83
|
+
| `submit` | Add new feedback from your terminal |
|
|
84
|
+
| `transcript` | Submit a customer interview, sales call, or support transcript |
|
|
56
85
|
|
|
57
|
-
|
|
86
|
+
**Share parameters** (when `action: "share"`):
|
|
58
87
|
|
|
59
|
-
|
|
88
|
+
| Parameter | Type | Description |
|
|
89
|
+
|-----------|------|-------------|
|
|
90
|
+
| `spec_id` | string | Spec ID (required) |
|
|
91
|
+
| `channel` | string | `email`, `widget`, `both`, `skip` |
|
|
92
|
+
| `message` | string | Custom message to include in notifications (optional) |
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
**Transcript parameters** (when `action: "transcript"`):
|
|
62
95
|
|
|
63
|
-
|
|
96
|
+
| Parameter | Type | Description |
|
|
97
|
+
|-----------|------|-------------|
|
|
98
|
+
| `text` | string | Full transcript text (min 50 chars, required) |
|
|
99
|
+
| `title` | string | Title, e.g. "Sales call with Acme Corp" |
|
|
100
|
+
| `type` | string | `interview`, `sales_call`, `support`, `other` |
|
|
101
|
+
| `customer_name` | string | Customer name (optional) |
|
|
102
|
+
| `customer_email` | string | Customer email (optional, for close-the-loop notifications) |
|
|
103
|
+
| `revenue_band` | string | `enterprise`, `paid`, `free` |
|
|
64
104
|
|
|
65
|
-
|
|
105
|
+
### `circuit.ask`
|
|
106
|
+
|
|
107
|
+
Semantic search across all your data: feedback, priorities, specs, and help articles. Returns your shipping patterns and behavioral insights. Also searches your connected GitHub repo for code context.
|
|
108
|
+
|
|
109
|
+
| Parameter | Type | Description |
|
|
110
|
+
|-----------|------|-------------|
|
|
111
|
+
| `question` | string | Natural language, e.g. "What are enterprise customers saying about onboarding?" |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Workflow
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
> circuit.priorities {weekly: true} # What changed since last week?
|
|
119
|
+
> circuit.priorities # See what matters
|
|
120
|
+
> circuit.spec {priority_id: "..."} # Get the spec
|
|
121
|
+
> circuit.act {action: "build", ...} # Start building
|
|
122
|
+
> ... write the code ...
|
|
123
|
+
> circuit.act {action: "ship", ...} # Ship it.
|
|
124
|
+
> circuit.act {action: "share", channel: "both"} # Share back. Customers notified.
|
|
125
|
+
> circuit.spec {batch: true, status: "shipped"} # Export shipped specs
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## CLI Commands
|
|
66
131
|
|
|
67
132
|
```bash
|
|
68
|
-
npx circuit-mcp
|
|
69
|
-
npx circuit-mcp setup
|
|
70
|
-
npx circuit-mcp auth
|
|
71
|
-
npx circuit-mcp logout
|
|
133
|
+
npx circuit-mcp # Start MCP server (stdio mode)
|
|
134
|
+
npx circuit-mcp setup # Interactive setup
|
|
135
|
+
npx circuit-mcp auth # Re-authenticate
|
|
136
|
+
npx circuit-mcp logout # Clear stored token
|
|
137
|
+
npx circuit-mcp fix # Troubleshooting guide
|
|
72
138
|
```
|
|
73
139
|
|
|
74
|
-
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Troubleshooting
|
|
143
|
+
|
|
144
|
+
**"Token expired"** — Run `npx circuit-mcp auth` to re-authenticate.
|
|
145
|
+
|
|
146
|
+
**"Connection refused"** — Check your internet connection. The MCP connects to Circuit's API.
|
|
147
|
+
|
|
148
|
+
**Tool not showing in Cursor/Claude** — Restart your editor after adding the MCP config.
|
|
149
|
+
|
|
150
|
+
**Custom API URL** — Set `CIRCUIT_API_URL` environment variable to override the default endpoint.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
75
155
|
|
|
76
156
|
```
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
157
|
+
Your editor (Cursor / Claude Code)
|
|
158
|
+
|
|
|
159
|
+
| JSON-RPC over stdio
|
|
160
|
+
|
|
|
161
|
+
circuit-mcp (this package)
|
|
162
|
+
|
|
|
163
|
+
| HTTPS + Bearer token
|
|
164
|
+
|
|
|
165
|
+
Circuit API
|
|
166
|
+
|
|
|
167
|
+
| PostgreSQL + pgvector
|
|
168
|
+
|
|
|
169
|
+
Your feedback data
|
|
88
170
|
```
|
|
89
171
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- [Circuit](https://withcircuit.com) - Customer feedback intelligence
|
|
93
|
-
- [MCP Protocol](https://modelcontextprotocol.io) - Model Context Protocol spec
|
|
172
|
+
The MCP server runs as a local process, communicating with your editor via stdin/stdout. All data stays in your Circuit account, authenticated via OAuth 2.0.
|
|
94
173
|
|
|
95
|
-
|
|
174
|
+
---
|
|
96
175
|
|
|
97
|
-
|
|
176
|
+
Built by [Circuit](https://withcircuit.com). Feedback in. Specs out.
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
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>
|
|
@@ -0,0 +1,57 @@
|
|
|
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/src/auth.js
CHANGED
|
@@ -8,7 +8,7 @@ import open from 'open';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { showSpinner, showInfo, showPrompt } from './ui.js';
|
|
10
10
|
|
|
11
|
-
const CIRCUIT_URL = 'https://app.withcircuit.com';
|
|
11
|
+
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
|
/**
|
|
@@ -136,79 +136,57 @@ function getSuccessPage() {
|
|
|
136
136
|
return `<!DOCTYPE html>
|
|
137
137
|
<html>
|
|
138
138
|
<head>
|
|
139
|
-
<title>Circuit - Connected
|
|
139
|
+
<title>Circuit - Connected</title>
|
|
140
140
|
<style>
|
|
141
141
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
142
142
|
body {
|
|
143
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
144
|
-
background: #
|
|
143
|
+
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
144
|
+
background: #F5F3F0;
|
|
145
145
|
min-height: 100vh;
|
|
146
146
|
display: flex;
|
|
147
147
|
align-items: center;
|
|
148
148
|
justify-content: center;
|
|
149
149
|
color: #1C1A18;
|
|
150
|
+
padding: 24px;
|
|
150
151
|
}
|
|
151
|
-
.
|
|
152
|
-
background: white;
|
|
153
|
-
border-radius: 16px;
|
|
154
|
-
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
|
155
|
-
padding: 48px 64px;
|
|
152
|
+
.content {
|
|
156
153
|
text-align: center;
|
|
157
|
-
max-width:
|
|
154
|
+
max-width: 400px;
|
|
158
155
|
}
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
align-items: center;
|
|
162
|
-
justify-content: center;
|
|
163
|
-
gap: 8px;
|
|
164
|
-
margin-bottom: 32px;
|
|
165
|
-
}
|
|
166
|
-
.logo-icon {
|
|
167
|
-
width: 28px;
|
|
168
|
-
height: 28px;
|
|
169
|
-
background: #1C1A18;
|
|
170
|
-
border-radius: 50%;
|
|
171
|
-
display: flex;
|
|
172
|
-
align-items: center;
|
|
173
|
-
justify-content: center;
|
|
174
|
-
}
|
|
175
|
-
.logo-icon::after {
|
|
176
|
-
content: '';
|
|
177
|
-
width: 12px;
|
|
178
|
-
height: 12px;
|
|
179
|
-
border: 2px solid white;
|
|
180
|
-
border-radius: 50%;
|
|
156
|
+
.icon {
|
|
157
|
+
margin-bottom: 24px;
|
|
181
158
|
}
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.woo {
|
|
187
|
-
font-size: 14px;
|
|
188
|
-
color: rgba(28,26,24,0.5);
|
|
189
|
-
margin-bottom: 8px;
|
|
159
|
+
.icon svg {
|
|
160
|
+
width: 48px;
|
|
161
|
+
height: 48px;
|
|
162
|
+
color: #1C1A18;
|
|
190
163
|
}
|
|
191
164
|
h1 {
|
|
192
|
-
font-size:
|
|
165
|
+
font-size: 24px;
|
|
193
166
|
font-weight: 600;
|
|
167
|
+
color: #1C1A18;
|
|
194
168
|
margin-bottom: 12px;
|
|
195
169
|
}
|
|
196
170
|
p {
|
|
197
|
-
font-size:
|
|
198
|
-
color: rgba(28,26,24,0.6);
|
|
199
|
-
line-height: 1.
|
|
171
|
+
font-size: 14px;
|
|
172
|
+
color: rgba(28, 26, 24, 0.6);
|
|
173
|
+
line-height: 1.6;
|
|
174
|
+
}
|
|
175
|
+
.hint {
|
|
176
|
+
font-size: 12px;
|
|
177
|
+
color: rgba(28, 26, 24, 0.6);
|
|
178
|
+
margin-top: 16px;
|
|
200
179
|
}
|
|
201
180
|
</style>
|
|
202
181
|
</head>
|
|
203
182
|
<body>
|
|
204
|
-
<div class="
|
|
205
|
-
<div class="
|
|
206
|
-
<
|
|
207
|
-
<span class="logo-text">Circuit</span>
|
|
183
|
+
<div class="content">
|
|
184
|
+
<div class="icon">
|
|
185
|
+
<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>
|
|
208
186
|
</div>
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
<p>
|
|
187
|
+
<h1>Connected.</h1>
|
|
188
|
+
<p>Your AI coding assistant is now connected to Circuit.</p>
|
|
189
|
+
<p class="hint">This window will close automatically...</p>
|
|
212
190
|
</div>
|
|
213
191
|
<script>setTimeout(() => window.close(), 3000);</script>
|
|
214
192
|
</body>
|
|
@@ -216,32 +194,65 @@ function getSuccessPage() {
|
|
|
216
194
|
}
|
|
217
195
|
|
|
218
196
|
function getErrorPage(error) {
|
|
197
|
+
// Sanitize error message to prevent XSS in the static HTML page
|
|
198
|
+
const safeError = String(error).replace(/[<>&"']/g, c => ({
|
|
199
|
+
'<': '<', '>': '>', '&': '&', '"': '"', "'": '''
|
|
200
|
+
})[c]);
|
|
201
|
+
|
|
219
202
|
return `<!DOCTYPE html>
|
|
220
203
|
<html>
|
|
221
204
|
<head>
|
|
222
|
-
<title>Circuit -
|
|
205
|
+
<title>Circuit - Connection Failed</title>
|
|
223
206
|
<style>
|
|
224
207
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
225
208
|
body {
|
|
226
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
227
|
-
background:
|
|
209
|
+
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
210
|
+
background: #F5F3F0;
|
|
228
211
|
min-height: 100vh;
|
|
229
212
|
display: flex;
|
|
230
213
|
align-items: center;
|
|
231
214
|
justify-content: center;
|
|
232
|
-
color:
|
|
215
|
+
color: #1C1A18;
|
|
216
|
+
padding: 24px;
|
|
217
|
+
}
|
|
218
|
+
.content {
|
|
219
|
+
text-align: center;
|
|
220
|
+
max-width: 400px;
|
|
221
|
+
}
|
|
222
|
+
.icon {
|
|
223
|
+
margin-bottom: 24px;
|
|
224
|
+
}
|
|
225
|
+
.icon svg {
|
|
226
|
+
width: 48px;
|
|
227
|
+
height: 48px;
|
|
228
|
+
color: #D64545;
|
|
229
|
+
}
|
|
230
|
+
h1 {
|
|
231
|
+
font-size: 24px;
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
color: #1C1A18;
|
|
234
|
+
margin-bottom: 12px;
|
|
235
|
+
}
|
|
236
|
+
p {
|
|
237
|
+
font-size: 14px;
|
|
238
|
+
color: rgba(28, 26, 24, 0.6);
|
|
239
|
+
line-height: 1.6;
|
|
240
|
+
}
|
|
241
|
+
.hint {
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
color: rgba(28, 26, 24, 0.6);
|
|
244
|
+
margin-top: 16px;
|
|
233
245
|
}
|
|
234
|
-
.container { text-align: center; padding: 48px; }
|
|
235
|
-
.icon { font-size: 64px; margin-bottom: 24px; }
|
|
236
|
-
h1 { font-size: 32px; color: #ef4444; margin-bottom: 16px; }
|
|
237
|
-
p { font-size: 18px; color: rgba(255,255,255,0.7); }
|
|
238
246
|
</style>
|
|
239
247
|
</head>
|
|
240
248
|
<body>
|
|
241
|
-
<div class="
|
|
242
|
-
<div class="icon"
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
<div class="content">
|
|
250
|
+
<div class="icon">
|
|
251
|
+
<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>
|
|
252
|
+
</div>
|
|
253
|
+
<h1>Connection Failed</h1>
|
|
254
|
+
<p>${safeError}</p>
|
|
255
|
+
<p class="hint">Please try connecting again from your AI assistant.</p>
|
|
245
256
|
</div>
|
|
246
257
|
</body>
|
|
247
258
|
</html>`;
|
package/src/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
2
|
|
|
3
|
-
const CIRCUIT_API = 'https://
|
|
3
|
+
const CIRCUIT_API = process.env.CIRCUIT_API_URL || 'https://api.withcircuit.com';
|
|
4
|
+
const API_TIMEOUT_MS = 30_000;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Start MCP server in stdio mode
|
|
@@ -13,7 +14,6 @@ export async function startMcpServer(token) {
|
|
|
13
14
|
terminal: false
|
|
14
15
|
});
|
|
15
16
|
|
|
16
|
-
// Handle incoming JSON-RPC messages
|
|
17
17
|
rl.on('line', async (line) => {
|
|
18
18
|
try {
|
|
19
19
|
const message = JSON.parse(line);
|
|
@@ -22,7 +22,6 @@ export async function startMcpServer(token) {
|
|
|
22
22
|
console.log(JSON.stringify(response));
|
|
23
23
|
}
|
|
24
24
|
} catch (err) {
|
|
25
|
-
// Send error response
|
|
26
25
|
console.log(JSON.stringify({
|
|
27
26
|
jsonrpc: '2.0',
|
|
28
27
|
error: { code: -32700, message: 'Parse error' },
|
|
@@ -31,32 +30,45 @@ export async function startMcpServer(token) {
|
|
|
31
30
|
}
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
// Keep process alive
|
|
35
33
|
process.stdin.resume();
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
* Call Circuit
|
|
37
|
+
* Call Circuit backend API with timeout
|
|
40
38
|
*/
|
|
41
39
|
async function callMcpApi(token, toolName, args = {}) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
headers: {
|
|
45
|
-
'Authorization': `Bearer ${token}`,
|
|
46
|
-
'Content-Type': 'application/json'
|
|
47
|
-
},
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
tool: toolName,
|
|
50
|
-
arguments: args
|
|
51
|
-
})
|
|
52
|
-
});
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(`${CIRCUIT_API}/mcp/call`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Authorization': `Bearer ${token}`,
|
|
48
|
+
'Content-Type': 'application/json'
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ tool: toolName, arguments: args }),
|
|
51
|
+
signal: controller.signal
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (response.status === 401) {
|
|
55
|
+
throw new Error('Token expired. Run: npx circuit-mcp auth');
|
|
56
|
+
}
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const text = await response.text();
|
|
60
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return response.json();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.name === 'AbortError') {
|
|
66
|
+
throw new Error('Request timed out. Check your connection and try again.');
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
/**
|
|
@@ -72,14 +84,8 @@ async function handleMessage(message, token) {
|
|
|
72
84
|
id,
|
|
73
85
|
result: {
|
|
74
86
|
protocolVersion: '2024-11-05',
|
|
75
|
-
serverInfo: {
|
|
76
|
-
|
|
77
|
-
version: '2.0.0'
|
|
78
|
-
},
|
|
79
|
-
capabilities: {
|
|
80
|
-
tools: {},
|
|
81
|
-
resources: {}
|
|
82
|
-
}
|
|
87
|
+
serverInfo: { name: 'circuit-mcp', version: '2.2.0' },
|
|
88
|
+
capabilities: { tools: {}, resources: {} }
|
|
83
89
|
}
|
|
84
90
|
};
|
|
85
91
|
|
|
@@ -87,142 +93,16 @@ async function handleMessage(message, token) {
|
|
|
87
93
|
return null;
|
|
88
94
|
|
|
89
95
|
case 'tools/list':
|
|
90
|
-
return {
|
|
91
|
-
jsonrpc: '2.0',
|
|
92
|
-
id,
|
|
93
|
-
result: {
|
|
94
|
-
tools: [
|
|
95
|
-
{
|
|
96
|
-
name: 'circuit.priorities',
|
|
97
|
-
description: 'What should I work on? Get ranked priorities with confidence indicators, trends, and memory context.',
|
|
98
|
-
inputSchema: {
|
|
99
|
-
type: 'object',
|
|
100
|
-
properties: {
|
|
101
|
-
lens: {
|
|
102
|
-
type: 'string',
|
|
103
|
-
description: "Focus lens: 'volume', 'urgency', 'revenue', 'retention', 'delight', 'feature'",
|
|
104
|
-
enum: ['volume', 'urgency', 'revenue', 'retention', 'delight', 'feature'],
|
|
105
|
-
default: 'volume'
|
|
106
|
-
},
|
|
107
|
-
segment: {
|
|
108
|
-
type: 'string',
|
|
109
|
-
description: "Filter by customer segment: 'enterprise', 'smb', 'all'",
|
|
110
|
-
default: 'all'
|
|
111
|
-
},
|
|
112
|
-
limit: {
|
|
113
|
-
type: 'number',
|
|
114
|
-
description: 'Number of priorities (default: 5, max: 20)',
|
|
115
|
-
default: 5
|
|
116
|
-
},
|
|
117
|
-
category: {
|
|
118
|
-
type: 'string',
|
|
119
|
-
description: "Filter by category: 'Bug', 'Feature', 'Friction', 'Complaint', 'Praise'",
|
|
120
|
-
enum: ['Bug', 'Feature', 'Friction', 'Complaint', 'Praise']
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: 'circuit.brief',
|
|
127
|
-
description: 'Get the full engineering spec for a priority. Includes brief content, customer context, version history, and related memory (previous ships, outcomes).',
|
|
128
|
-
inputSchema: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: {
|
|
131
|
-
priority_id: {
|
|
132
|
-
type: 'string',
|
|
133
|
-
description: 'The priority ID'
|
|
134
|
-
},
|
|
135
|
-
build_id: {
|
|
136
|
-
type: 'string',
|
|
137
|
-
description: 'The build ID directly (alternative to priority_id)'
|
|
138
|
-
},
|
|
139
|
-
include_history: {
|
|
140
|
-
type: 'boolean',
|
|
141
|
-
description: 'Include version history and related memory',
|
|
142
|
-
default: true
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: 'circuit.act',
|
|
149
|
-
description: 'Take an action in Circuit. Ship a brief, start building, correct a classification, or submit new feedback.',
|
|
150
|
-
inputSchema: {
|
|
151
|
-
type: 'object',
|
|
152
|
-
properties: {
|
|
153
|
-
action: {
|
|
154
|
-
type: 'string',
|
|
155
|
-
description: "Action to take: 'build' (start building), 'ship' (mark shipped), 'correct' (fix classification), 'submit' (add feedback)",
|
|
156
|
-
enum: ['build', 'ship', 'correct', 'submit']
|
|
157
|
-
},
|
|
158
|
-
brief_id: {
|
|
159
|
-
type: 'string',
|
|
160
|
-
description: "Brief ID (for 'build' and 'ship' actions)"
|
|
161
|
-
},
|
|
162
|
-
priority_id: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
description: "Priority ID (for 'correct' action)"
|
|
165
|
-
},
|
|
166
|
-
correction_type: {
|
|
167
|
-
type: 'string',
|
|
168
|
-
description: "What to correct (for 'correct' action): 'category'",
|
|
169
|
-
enum: ['category']
|
|
170
|
-
},
|
|
171
|
-
original: {
|
|
172
|
-
type: 'string',
|
|
173
|
-
description: 'Original value being corrected'
|
|
174
|
-
},
|
|
175
|
-
corrected: {
|
|
176
|
-
type: 'string',
|
|
177
|
-
description: 'New corrected value'
|
|
178
|
-
},
|
|
179
|
-
feedback: {
|
|
180
|
-
type: 'string',
|
|
181
|
-
description: "Feedback text (for 'submit' action)"
|
|
182
|
-
},
|
|
183
|
-
source: {
|
|
184
|
-
type: 'string',
|
|
185
|
-
description: "Feedback source (for 'submit' action)",
|
|
186
|
-
default: 'mcp'
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
required: ['action']
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: 'circuit.ask',
|
|
194
|
-
description: 'Ask anything about your feedback data. Searches across feedback, priorities, briefs, and help articles using semantic search.',
|
|
195
|
-
inputSchema: {
|
|
196
|
-
type: 'object',
|
|
197
|
-
properties: {
|
|
198
|
-
question: {
|
|
199
|
-
type: 'string',
|
|
200
|
-
description: "Natural language question (e.g., 'What are enterprise customers complaining about?', 'How do briefs work?')"
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
required: ['question']
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
]
|
|
207
|
-
}
|
|
208
|
-
};
|
|
96
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
209
97
|
|
|
210
98
|
case 'tools/call':
|
|
211
99
|
return await handleToolCall(id, params, token);
|
|
212
100
|
|
|
213
101
|
case 'resources/list':
|
|
214
|
-
return {
|
|
215
|
-
jsonrpc: '2.0',
|
|
216
|
-
id,
|
|
217
|
-
result: { resources: [] }
|
|
218
|
-
};
|
|
102
|
+
return { jsonrpc: '2.0', id, result: { resources: [] } };
|
|
219
103
|
|
|
220
104
|
case 'ping':
|
|
221
|
-
return {
|
|
222
|
-
jsonrpc: '2.0',
|
|
223
|
-
id,
|
|
224
|
-
result: {}
|
|
225
|
-
};
|
|
105
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
226
106
|
|
|
227
107
|
default:
|
|
228
108
|
return {
|
|
@@ -233,96 +113,264 @@ async function handleMessage(message, token) {
|
|
|
233
113
|
}
|
|
234
114
|
}
|
|
235
115
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
116
|
+
// ─────────────────────────────────────────────────────────────
|
|
117
|
+
// Tool Definitions
|
|
118
|
+
// ─────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
const TOOLS = [
|
|
121
|
+
{
|
|
122
|
+
name: 'circuit.priorities',
|
|
123
|
+
description: 'What should I work on? Ranked customer priorities with trend data, confidence indicators, and pattern matching. Set weekly: true to get the weekly digest instead.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
weekly: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Set to true to get the weekly digest: priority movements, new entries, dropped items, and volume spikes compared to last week'
|
|
130
|
+
},
|
|
131
|
+
lens: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: "How to rank: 'volume' (most users), 'urgency' (bugs & quality), 'revenue' (revenue impact), 'retention' (negative sentiment), 'delight' (positive sentiment), 'feature' (new feature requests)",
|
|
134
|
+
enum: ['volume', 'urgency', 'revenue', 'retention', 'delight', 'feature'],
|
|
135
|
+
default: 'volume'
|
|
136
|
+
},
|
|
137
|
+
segment: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: "Customer segment filter: 'enterprise', 'smb', 'all'",
|
|
140
|
+
default: 'all'
|
|
141
|
+
},
|
|
142
|
+
limit: {
|
|
143
|
+
type: 'number',
|
|
144
|
+
description: 'Number of priorities to return (default: 5, max: 20)',
|
|
145
|
+
default: 5
|
|
146
|
+
},
|
|
147
|
+
category: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: "Filter by feedback category",
|
|
150
|
+
enum: ['Bug', 'Feature', 'Improvement', 'Praise']
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'circuit.spec',
|
|
157
|
+
description: 'Full engineering spec for a priority. 5 sections: What to Build, Why It Matters, Customer Voice, Files to Touch, Done When. Set batch: true to export multiple specs as markdown.',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
priority_id: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'The priority ID (from circuit.priorities)'
|
|
164
|
+
},
|
|
165
|
+
spec_id: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The spec ID directly (alternative to priority_id)'
|
|
168
|
+
},
|
|
169
|
+
include_history: {
|
|
170
|
+
type: 'boolean',
|
|
171
|
+
description: 'Include version history and related ship memory',
|
|
172
|
+
default: true
|
|
173
|
+
},
|
|
174
|
+
batch: {
|
|
175
|
+
type: 'boolean',
|
|
176
|
+
description: 'Set to true to export multiple specs as markdown for sprint planning'
|
|
177
|
+
},
|
|
178
|
+
spec_ids: {
|
|
179
|
+
type: 'array',
|
|
180
|
+
items: { type: 'string' },
|
|
181
|
+
description: 'Specific spec IDs to export (batch mode). If empty, exports recent specs.'
|
|
182
|
+
},
|
|
183
|
+
status: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: "Filter by status (batch mode): 'ready', 'building', 'shipped'",
|
|
186
|
+
enum: ['ready', 'building', 'shipped', 'done']
|
|
187
|
+
},
|
|
188
|
+
limit: {
|
|
189
|
+
type: 'number',
|
|
190
|
+
description: 'Number of specs to export in batch mode (default: 10, max: 50)',
|
|
191
|
+
default: 10
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'circuit.act',
|
|
198
|
+
description: "Take action. Start building, ship it, share back with customers, assign, correct a classification, submit feedback, or submit a transcript.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
action: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
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)",
|
|
205
|
+
enum: ['build', 'ship', 'share', 'assign', 'correct', 'submit', 'transcript']
|
|
206
|
+
},
|
|
207
|
+
spec_id: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: "Spec ID (for build, ship, share, assign)"
|
|
210
|
+
},
|
|
211
|
+
channel: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
description: "Notification channel for share: 'email', 'widget', 'both', 'skip'",
|
|
214
|
+
enum: ['email', 'widget', 'both', 'skip']
|
|
215
|
+
},
|
|
216
|
+
message: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: "Custom message to include in share notifications (optional)"
|
|
219
|
+
},
|
|
220
|
+
assigned_to: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: "Team member email or user ID (for assign). Empty to unassign."
|
|
223
|
+
},
|
|
224
|
+
priority_id: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: "Priority ID (for correct)"
|
|
227
|
+
},
|
|
228
|
+
correction_type: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: "What to correct: 'category'",
|
|
231
|
+
enum: ['category']
|
|
232
|
+
},
|
|
233
|
+
original: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'Original value being corrected'
|
|
236
|
+
},
|
|
237
|
+
corrected: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'New corrected value'
|
|
240
|
+
},
|
|
241
|
+
feedback: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
description: "Feedback text (for submit)"
|
|
244
|
+
},
|
|
245
|
+
source: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description: "Feedback source (for submit)",
|
|
248
|
+
default: 'mcp'
|
|
249
|
+
},
|
|
250
|
+
text: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Full transcript text, min 50 chars (for transcript)'
|
|
253
|
+
},
|
|
254
|
+
title: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: "Transcript title, e.g. 'Sales call with Acme Corp' (for transcript)"
|
|
257
|
+
},
|
|
258
|
+
type: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: "Transcript type (for transcript)",
|
|
261
|
+
enum: ['interview', 'sales_call', 'support', 'other']
|
|
262
|
+
},
|
|
263
|
+
customer_name: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Customer name (for transcript, optional)'
|
|
266
|
+
},
|
|
267
|
+
customer_email: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'Customer email (for transcript, optional)'
|
|
270
|
+
},
|
|
271
|
+
revenue_band: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: "Customer segment (for transcript): 'enterprise', 'paid', 'free'",
|
|
274
|
+
enum: ['enterprise', 'paid', 'free']
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
required: ['action']
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'circuit.ask',
|
|
282
|
+
description: 'Search across all your feedback data. Semantic search over feedback, priorities, briefs, and help articles. Also returns your shipping patterns and behavioral insights.',
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
question: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: "Natural language question, e.g. 'What are enterprise customers complaining about?'"
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
required: ['question']
|
|
292
|
+
}
|
|
242
293
|
}
|
|
294
|
+
];
|
|
243
295
|
|
|
244
|
-
|
|
245
|
-
|
|
296
|
+
// ─────────────────────────────────────────────────────────────
|
|
297
|
+
// Response Formatting
|
|
298
|
+
// ─────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
function formatPriorities(data) {
|
|
301
|
+
// Weekly digest mode — detected by presence of movements/new_entries/headline
|
|
302
|
+
if (data.headline !== undefined || data.movements !== undefined) {
|
|
303
|
+
return formatWeeklyDigest(data);
|
|
246
304
|
}
|
|
247
305
|
|
|
306
|
+
if (data.message) return data.message;
|
|
307
|
+
if (!data.priorities?.length) return 'No priorities found. Upload feedback to get started.';
|
|
308
|
+
|
|
248
309
|
const lines = [];
|
|
249
310
|
|
|
250
311
|
if (data.memory_applied) {
|
|
251
|
-
lines.push(
|
|
312
|
+
lines.push(`*${data.ships_count} ships tracked · segment: ${data.segment_affinity || 'mixed'}*\n`);
|
|
252
313
|
}
|
|
253
314
|
|
|
254
315
|
for (const p of data.priorities) {
|
|
255
|
-
let trendText = '';
|
|
256
|
-
if (p.trend === 'up') {
|
|
257
|
-
trendText = ` ↑${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
|
|
258
|
-
} else if (p.trend === 'down') {
|
|
259
|
-
trendText = ` ↓${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
316
|
lines.push(`**#${p.rank}. ${p.theme}**`);
|
|
263
317
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (trendText) badges.push(trendText.trim());
|
|
318
|
+
const meta = [p.category || 'Other', `${p.volume} users`];
|
|
319
|
+
if (p.trend === 'up') meta.push(`↑${p.trend_percent ? ` ${p.trend_percent}%` : ''}`);
|
|
320
|
+
if (p.trend === 'down') meta.push(`↓${p.trend_percent ? ` ${p.trend_percent}%` : ''}`);
|
|
268
321
|
if (p.brief_status && p.brief_status !== 'no_brief') {
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
}
|
|
272
|
-
if (p.matches_pattern) badges.push('matches pattern');
|
|
273
|
-
if (p.version) badges.push(p.version);
|
|
274
|
-
|
|
275
|
-
lines.push(badges.join(' · '));
|
|
276
|
-
|
|
277
|
-
if (p.key_quote) {
|
|
278
|
-
lines.push(`> "${p.key_quote}"`);
|
|
322
|
+
const label = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[p.brief_status];
|
|
323
|
+
if (label) meta.push(label);
|
|
279
324
|
}
|
|
325
|
+
if (p.matches_pattern) meta.push('matches pattern');
|
|
326
|
+
if (p.version) meta.push(p.version);
|
|
327
|
+
lines.push(meta.join(' · '));
|
|
280
328
|
|
|
281
|
-
lines.push(
|
|
329
|
+
if (p.key_quote) lines.push(`> "${p.key_quote}"`);
|
|
330
|
+
lines.push(`priority_id: \`${p.priority_id}\`${p.build_id ? ` · spec_id: \`${p.build_id}\`` : ''}`);
|
|
282
331
|
lines.push('');
|
|
283
332
|
}
|
|
284
333
|
|
|
285
334
|
lines.push('---');
|
|
286
|
-
lines.push('Use `circuit.
|
|
335
|
+
lines.push('Use `circuit.spec` with a priority_id to get the full spec.');
|
|
287
336
|
|
|
288
337
|
return lines.join('\n');
|
|
289
338
|
}
|
|
290
339
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
340
|
+
function formatSpec(data) {
|
|
341
|
+
// Batch export mode — detected by presence of markdown/count
|
|
342
|
+
if (data.markdown !== undefined) {
|
|
343
|
+
return formatBatchExport(data);
|
|
344
|
+
}
|
|
345
|
+
|
|
295
346
|
if (data.error) {
|
|
296
347
|
if (data.error === 'no_brief' && data.priority) {
|
|
297
348
|
const p = data.priority;
|
|
298
|
-
|
|
299
|
-
output += `${p.category || 'Other'} · ${p.volume || 0} users\n\n`;
|
|
300
|
-
output += `**No brief generated yet.**\n\n`;
|
|
301
|
-
if (data.suggestion) {
|
|
302
|
-
output += `${data.suggestion}\n`;
|
|
303
|
-
}
|
|
304
|
-
return output;
|
|
349
|
+
return `# ${p.theme || 'Priority'}\n\n${p.category || 'Other'} · ${p.volume || 0} users\n\n**No brief generated yet.**\n\n${data.suggestion || ''}`;
|
|
305
350
|
}
|
|
306
351
|
return `Error: ${data.message || data.error}`;
|
|
307
352
|
}
|
|
308
353
|
|
|
309
354
|
const spec = data.spec_content || '';
|
|
310
|
-
let output = '';
|
|
311
|
-
|
|
312
355
|
const title = data.title || 'Engineering Brief';
|
|
313
|
-
|
|
356
|
+
const status = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[data.status] || data.status;
|
|
357
|
+
const version = data.version_badge ? ` · ${data.version_badge}` : '';
|
|
314
358
|
|
|
315
|
-
|
|
316
|
-
const versionBadge = data.version_badge ? ` · ${data.version_badge}` : '';
|
|
317
|
-
output += `Status: ${statusText}${versionBadge}\n`;
|
|
359
|
+
let output = `# ${title}\n\nStatus: ${status}${version}\n`;
|
|
318
360
|
|
|
319
361
|
if (data.customer_context) {
|
|
320
362
|
const ctx = data.customer_context;
|
|
321
363
|
output += `${ctx.category} · ${ctx.volume} users · ${ctx.paying_percent}% paying\n`;
|
|
322
364
|
}
|
|
365
|
+
|
|
366
|
+
if (data.effort) {
|
|
367
|
+
const e = data.effort;
|
|
368
|
+
output += `Effort: ${e.files} file${e.files !== 1 ? 's' : ''} · ${e.label}\n`;
|
|
369
|
+
}
|
|
370
|
+
|
|
323
371
|
output += '\n';
|
|
324
372
|
|
|
325
|
-
|
|
373
|
+
output += spec
|
|
326
374
|
.replace(/<what_to_build>/gi, '## WHAT TO BUILD\n')
|
|
327
375
|
.replace(/<\/what_to_build>/gi, '\n')
|
|
328
376
|
.replace(/<why_it_matters>/gi, '## WHY IT MATTERS\n')
|
|
@@ -334,39 +382,31 @@ function formatBrief(data) {
|
|
|
334
382
|
.replace(/<done_when>/gi, '## DONE WHEN\n')
|
|
335
383
|
.replace(/<\/done_when>/gi, '\n');
|
|
336
384
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (data.related_memory && data.related_memory.length > 0) {
|
|
385
|
+
if (data.related_memory?.length > 0) {
|
|
340
386
|
output += '\n## WHAT CIRCUIT REMEMBERS\n\n';
|
|
341
387
|
for (const mem of data.related_memory) {
|
|
342
|
-
|
|
343
|
-
output += `- Shipped: ${mem.theme || mem.summary || 'Related feature'}\n`;
|
|
344
|
-
} else if (mem.type === 'correction') {
|
|
345
|
-
output += `- Correction: ${mem.summary || 'Classification adjusted'}\n`;
|
|
346
|
-
} else {
|
|
347
|
-
output += `- ${mem.summary || JSON.stringify(mem)}\n`;
|
|
348
|
-
}
|
|
388
|
+
output += `- ${mem.summary || mem.theme || JSON.stringify(mem)}\n`;
|
|
349
389
|
}
|
|
350
390
|
}
|
|
351
391
|
|
|
352
|
-
output += `\n---\
|
|
353
|
-
output += `build_id: \`${data.build_id}\``;
|
|
392
|
+
output += `\n---\nspec_id: \`${data.build_id}\``;
|
|
354
393
|
|
|
355
394
|
return output;
|
|
356
395
|
}
|
|
357
396
|
|
|
358
|
-
/**
|
|
359
|
-
* Format act response
|
|
360
|
-
*/
|
|
361
397
|
function formatAct(data) {
|
|
362
|
-
if (data.error) {
|
|
363
|
-
return `Error: ${data.error}`;
|
|
364
|
-
}
|
|
398
|
+
if (data.error) return `Error: ${data.error}`;
|
|
365
399
|
|
|
366
400
|
if (data.success) {
|
|
367
401
|
let output = data.message;
|
|
368
402
|
if (data.memory_created) {
|
|
369
|
-
output += '\nShip memory recorded
|
|
403
|
+
output += '\nShip memory recorded. Circuit will remember this for future specs.';
|
|
404
|
+
}
|
|
405
|
+
if (data.transcript_id) {
|
|
406
|
+
output += `\n\ntranscript_id: \`${data.transcript_id}\``;
|
|
407
|
+
}
|
|
408
|
+
if (data.spec_id && data.action === 'share') {
|
|
409
|
+
output += `\n\nspec_id: \`${data.spec_id}\``;
|
|
370
410
|
}
|
|
371
411
|
return output;
|
|
372
412
|
}
|
|
@@ -374,17 +414,12 @@ function formatAct(data) {
|
|
|
374
414
|
return JSON.stringify(data, null, 2);
|
|
375
415
|
}
|
|
376
416
|
|
|
377
|
-
/**
|
|
378
|
-
* Format ask response
|
|
379
|
-
*/
|
|
380
417
|
function formatAsk(data) {
|
|
381
|
-
if (data.message)
|
|
382
|
-
return data.message;
|
|
383
|
-
}
|
|
418
|
+
if (data.message) return data.message;
|
|
384
419
|
|
|
385
420
|
const lines = [];
|
|
386
421
|
|
|
387
|
-
if (data.help_articles
|
|
422
|
+
if (data.help_articles?.length > 0) {
|
|
388
423
|
lines.push('**Help Articles:**');
|
|
389
424
|
for (const a of data.help_articles) {
|
|
390
425
|
lines.push(`- **${a.title}**: ${a.content}`);
|
|
@@ -392,15 +427,15 @@ function formatAsk(data) {
|
|
|
392
427
|
lines.push('');
|
|
393
428
|
}
|
|
394
429
|
|
|
395
|
-
if (data.priorities
|
|
430
|
+
if (data.priorities?.length > 0) {
|
|
396
431
|
lines.push('**Related Priorities:**');
|
|
397
432
|
for (const p of data.priorities) {
|
|
398
|
-
lines.push(`- ${p.theme} (${p.category}, ${p.volume} users) —
|
|
433
|
+
lines.push(`- ${p.theme} (${p.category}, ${p.volume} users) — \`${p.id}\``);
|
|
399
434
|
}
|
|
400
435
|
lines.push('');
|
|
401
436
|
}
|
|
402
437
|
|
|
403
|
-
if (data.feedback
|
|
438
|
+
if (data.feedback?.length > 0) {
|
|
404
439
|
lines.push('**Related Feedback:**');
|
|
405
440
|
for (const f of data.feedback) {
|
|
406
441
|
lines.push(`- [${f.source}] "${f.text}"`);
|
|
@@ -415,7 +450,7 @@ function formatAsk(data) {
|
|
|
415
450
|
lines.push(`- Segment: ${pat.segment_affinity}`);
|
|
416
451
|
if (pat.top_categories) {
|
|
417
452
|
const cats = Object.entries(pat.top_categories).map(([k, v]) => `${k} (${Math.round(v * 100)}%)`).join(', ');
|
|
418
|
-
lines.push(`-
|
|
453
|
+
lines.push(`- Categories: ${cats}`);
|
|
419
454
|
}
|
|
420
455
|
lines.push('');
|
|
421
456
|
}
|
|
@@ -424,45 +459,95 @@ function formatAsk(data) {
|
|
|
424
459
|
return `No results found for "${data.question}". Try rephrasing.`;
|
|
425
460
|
}
|
|
426
461
|
|
|
427
|
-
lines.push(`---\n${data.total} results
|
|
462
|
+
lines.push(`---\n${data.total} results`);
|
|
463
|
+
return lines.join('\n');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Internal helper: format weekly digest data (called from formatPriorities when weekly: true)
|
|
467
|
+
function formatWeeklyDigest(data) {
|
|
468
|
+
if (data.error) return `Error: ${data.error}`;
|
|
469
|
+
|
|
470
|
+
const lines = [];
|
|
471
|
+
|
|
472
|
+
lines.push(`**${data.headline || 'Weekly Digest'}**\n`);
|
|
473
|
+
|
|
474
|
+
if (data.movements?.length > 0) {
|
|
475
|
+
lines.push('**Movements:**');
|
|
476
|
+
for (const m of data.movements) {
|
|
477
|
+
const arrow = m.direction === 'up' ? '↑' : '↓';
|
|
478
|
+
lines.push(`- ${arrow} ${m.title} (#${m.from_rank} → #${m.to_rank})`);
|
|
479
|
+
}
|
|
480
|
+
lines.push('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (data.new_entries?.length > 0) {
|
|
484
|
+
lines.push('**New This Week:**');
|
|
485
|
+
for (const n of data.new_entries) {
|
|
486
|
+
lines.push(`- ${n.title} (rank #${n.rank}, ${n.mentions} mentions)`);
|
|
487
|
+
}
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (data.volume_spikes?.length > 0) {
|
|
492
|
+
lines.push('**Volume Spikes:**');
|
|
493
|
+
for (const v of data.volume_spikes) {
|
|
494
|
+
lines.push(`- ${v.title} (+${v.percent_increase}%)`);
|
|
495
|
+
}
|
|
496
|
+
lines.push('');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (data.dropped?.length > 0) {
|
|
500
|
+
lines.push('**Dropped Off:**');
|
|
501
|
+
for (const d of data.dropped) {
|
|
502
|
+
lines.push(`- ${d.title}${d.reason ? ` (${d.reason})` : ''}`);
|
|
503
|
+
}
|
|
504
|
+
lines.push('');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!data.has_previous) {
|
|
508
|
+
lines.push('*First week — baseline captured. Changes will appear next week.*');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (lines.length <= 1) {
|
|
512
|
+
return data.headline || 'No changes this week.';
|
|
513
|
+
}
|
|
428
514
|
|
|
429
515
|
return lines.join('\n');
|
|
430
516
|
}
|
|
431
517
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
518
|
+
// Internal helper: format batch export data (called from formatSpec when batch: true)
|
|
519
|
+
function formatBatchExport(data) {
|
|
520
|
+
if (data.error) return `Error: ${data.error}${data.suggestion ? `\n${data.suggestion}` : ''}`;
|
|
521
|
+
|
|
522
|
+
let output = `**${data.count} brief${data.count !== 1 ? 's' : ''} exported**\n\n`;
|
|
523
|
+
output += data.markdown;
|
|
524
|
+
return output;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ─────────────────────────────────────────────────────────────
|
|
528
|
+
// Tool Call Handler
|
|
529
|
+
// ─────────────────────────────────────────────────────────────
|
|
530
|
+
|
|
435
531
|
async function handleToolCall(id, params, token) {
|
|
436
532
|
const { name, arguments: args } = params;
|
|
437
533
|
|
|
438
534
|
try {
|
|
439
535
|
const result = await callMcpApi(token, name, args || {});
|
|
440
536
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
case 'circuit.act':
|
|
451
|
-
formattedText = formatAct(result);
|
|
452
|
-
break;
|
|
453
|
-
case 'circuit.ask':
|
|
454
|
-
formattedText = formatAsk(result);
|
|
455
|
-
break;
|
|
456
|
-
default:
|
|
457
|
-
formattedText = JSON.stringify(result, null, 2);
|
|
458
|
-
}
|
|
537
|
+
const formatters = {
|
|
538
|
+
'circuit.priorities': formatPriorities,
|
|
539
|
+
'circuit.spec': formatSpec,
|
|
540
|
+
'circuit.act': formatAct,
|
|
541
|
+
'circuit.ask': formatAsk,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const formatter = formatters[name] || ((d) => JSON.stringify(d, null, 2));
|
|
545
|
+
const text = formatter(result);
|
|
459
546
|
|
|
460
547
|
return {
|
|
461
548
|
jsonrpc: '2.0',
|
|
462
549
|
id,
|
|
463
|
-
result: {
|
|
464
|
-
content: [{ type: 'text', text: formattedText }]
|
|
465
|
-
}
|
|
550
|
+
result: { content: [{ type: 'text', text }] }
|
|
466
551
|
};
|
|
467
552
|
|
|
468
553
|
} catch (err) {
|