circuit-mcp 1.0.16 → 2.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/publish.sh +30 -0
  3. package/src/server.js +148 -170
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuit-mcp",
3
- "version": "1.0.16",
3
+ "version": "2.0.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": {
package/publish.sh ADDED
@@ -0,0 +1,30 @@
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
+
package/src/server.js CHANGED
@@ -74,7 +74,7 @@ async function handleMessage(message, token) {
74
74
  protocolVersion: '2024-11-05',
75
75
  serverInfo: {
76
76
  name: 'circuit-mcp',
77
- version: '1.0.0'
77
+ version: '2.0.0'
78
78
  },
79
79
  capabilities: {
80
80
  tools: {},
@@ -84,7 +84,6 @@ async function handleMessage(message, token) {
84
84
  };
85
85
 
86
86
  case 'initialized':
87
- // No response needed for notification
88
87
  return null;
89
88
 
90
89
  case 'tools/list':
@@ -94,97 +93,114 @@ async function handleMessage(message, token) {
94
93
  result: {
95
94
  tools: [
96
95
  {
97
- name: 'get_priorities',
98
- description: 'Get your top priorities from Circuit, ranked by customer volume. These are the features/fixes that matter most to your users.',
96
+ name: 'circuit.priorities',
97
+ description: 'What should I work on? Get ranked priorities with confidence indicators, trends, and memory context.',
99
98
  inputSchema: {
100
99
  type: 'object',
101
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
+ },
102
112
  limit: {
103
113
  type: 'number',
104
- description: 'Number of priorities to return (default: 5, max: 20)'
114
+ description: 'Number of priorities (default: 5, max: 20)',
115
+ default: 5
105
116
  },
106
- sort_by: {
117
+ category: {
107
118
  type: 'string',
108
- description: 'Sort criterion: volume, urgency, revenue, negative, positive',
109
- enum: ['volume', 'urgency', 'revenue', 'negative', 'positive']
119
+ description: "Filter by category: 'Bug', 'Feature', 'Friction', 'Complaint', 'Praise'",
120
+ enum: ['Bug', 'Feature', 'Friction', 'Complaint', 'Praise']
110
121
  }
111
122
  }
112
123
  }
113
124
  },
114
125
  {
115
- name: 'get_brief',
116
- description: 'Get the full engineering brief for a specific priority. Includes what to build, why it matters, customer quotes, files to touch, and done criteria.',
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).',
117
128
  inputSchema: {
118
129
  type: 'object',
119
130
  properties: {
120
131
  priority_id: {
121
132
  type: 'string',
122
- description: 'The priority ID (from get_priorities)'
133
+ description: 'The priority ID'
123
134
  },
124
135
  build_id: {
125
136
  type: 'string',
126
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
127
143
  }
128
144
  }
129
145
  }
130
146
  },
131
147
  {
132
- name: 'start_building',
133
- description: "Mark a brief as 'building' - you're actively working on this feature.",
148
+ name: 'circuit.act',
149
+ description: 'Take an action in Circuit. Ship a brief, start building, correct a classification, or submit new feedback.',
134
150
  inputSchema: {
135
151
  type: 'object',
136
152
  properties: {
137
- build_id: {
153
+ action: {
138
154
  type: 'string',
139
- description: 'The build ID to mark as building'
140
- }
141
- },
142
- required: ['build_id']
143
- }
144
- },
145
- {
146
- name: 'mark_done',
147
- description: "Mark a brief as 'done' - the feature has shipped! This closes the feedback loop.",
148
- inputSchema: {
149
- type: 'object',
150
- properties: {
151
- build_id: {
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: {
152
159
  type: 'string',
153
- description: 'The build ID to mark as done'
154
- }
155
- },
156
- required: ['build_id']
157
- }
158
- },
159
- {
160
- name: 'search_feedback',
161
- description: 'Search customer feedback by keyword. Useful for understanding pain points around specific features.',
162
- inputSchema: {
163
- type: 'object',
164
- properties: {
165
- query: {
160
+ description: "Brief ID (for 'build' and 'ship' actions)"
161
+ },
162
+ priority_id: {
166
163
  type: 'string',
167
- description: 'Search query (keywords or phrase)'
164
+ description: "Priority ID (for 'correct' action)"
168
165
  },
169
- limit: {
170
- type: 'number',
171
- description: 'Max results (default: 10)'
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'
172
187
  }
173
188
  },
174
- required: ['query']
189
+ required: ['action']
175
190
  }
176
191
  },
177
192
  {
178
- name: 'get_insights',
179
- description: 'Get high-level insights and patterns across your priorities. Identifies themes, trends, and strategic recommendations.',
193
+ name: 'circuit.ask',
194
+ description: 'Ask anything about your feedback data. Searches across feedback, priorities, briefs, and help articles using semantic search.',
180
195
  inputSchema: {
181
196
  type: 'object',
182
197
  properties: {
183
- limit: {
184
- type: 'number',
185
- description: 'Number of priorities to analyze (default: 10)'
198
+ question: {
199
+ type: 'string',
200
+ description: "Natural language question (e.g., 'What are enterprise customers complaining about?', 'How do briefs work?')"
186
201
  }
187
- }
202
+ },
203
+ required: ['question']
188
204
  }
189
205
  }
190
206
  ]
@@ -218,7 +234,7 @@ async function handleMessage(message, token) {
218
234
  }
219
235
 
220
236
  /**
221
- * Format priorities for display (matches Circuit UI exactly)
237
+ * Format priorities response
222
238
  */
223
239
  function formatPriorities(data) {
224
240
  if (data.message) {
@@ -231,47 +247,49 @@ function formatPriorities(data) {
231
247
 
232
248
  const lines = [];
233
249
 
250
+ if (data.memory_applied) {
251
+ lines.push(`*Memory active: ${data.ships_count} ships tracked, segment: ${data.segment_affinity || 'mixed'}*\n`);
252
+ }
253
+
234
254
  for (const p of data.priorities) {
235
- // Format trend indicator like Circuit UI
236
255
  let trendText = '';
237
256
  if (p.trend === 'up') {
238
- trendText = ' ';
257
+ trendText = ` ↑${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
239
258
  } else if (p.trend === 'down') {
240
- trendText = ' ';
259
+ trendText = ` ↓${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
241
260
  }
242
261
 
243
- // Format: #Rank. Title
244
262
  lines.push(`**#${p.rank}. ${p.theme}**`);
245
263
 
246
- // Badges row: Category | X users | Trend | Status
247
264
  const badges = [];
248
265
  badges.push(p.category || 'Other');
249
266
  badges.push(`${p.volume} users`);
250
267
  if (trendText) badges.push(trendText.trim());
251
268
  if (p.brief_status && p.brief_status !== 'no_brief') {
252
- const statusLabel = { ready: 'Ready', building: 'Building', done: 'Done' }[p.brief_status];
269
+ const statusLabel = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[p.brief_status];
253
270
  if (statusLabel) badges.push(statusLabel);
254
271
  }
272
+ if (p.matches_pattern) badges.push('matches pattern');
273
+ if (p.version) badges.push(p.version);
255
274
 
256
275
  lines.push(badges.join(' · '));
257
276
 
258
- // Key quote (customer voice)
259
277
  if (p.key_quote) {
260
278
  lines.push(`> "${p.key_quote}"`);
261
279
  }
262
280
 
263
- lines.push(`priority_id: \`${p.id}\``);
281
+ lines.push(`priority_id: \`${p.priority_id}\`${p.build_id ? ` · build_id: \`${p.build_id}\`` : ''}`);
264
282
  lines.push('');
265
283
  }
266
284
 
267
285
  lines.push('---');
268
- lines.push('Use `get_brief` with a priority_id to see the full engineering spec.');
286
+ lines.push('Use `circuit.brief` with a priority_id to see the full engineering spec.');
269
287
 
270
288
  return lines.join('\n');
271
289
  }
272
290
 
273
291
  /**
274
- * Format brief for display (matches Circuit UI format for Cursor/Claude)
292
+ * Format brief response
275
293
  */
276
294
  function formatBrief(data) {
277
295
  if (data.error) {
@@ -279,50 +297,31 @@ function formatBrief(data) {
279
297
  const p = data.priority;
280
298
  let output = `# ${p.theme || 'Priority'}\n\n`;
281
299
  output += `${p.category || 'Other'} · ${p.volume || 0} users\n\n`;
282
- output += `**No brief generated yet**\n\n`;
283
-
284
- if (p.summary) {
285
- output += `${p.summary}\n\n`;
286
- }
287
-
288
- if (p.key_quote) {
289
- output += `> "${p.key_quote}"\n\n`;
300
+ output += `**No brief generated yet.**\n\n`;
301
+ if (data.suggestion) {
302
+ output += `${data.suggestion}\n`;
290
303
  }
291
-
292
- output += `---\n\n`;
293
-
294
- // Include deep link to Circuit
295
- if (data.circuit_url) {
296
- output += `**Generate brief:** ${data.circuit_url}\n\n`;
297
- }
298
-
299
- output += `Or I can help you draft implementation notes based on the customer feedback above.`;
300
304
  return output;
301
305
  }
302
306
  return `Error: ${data.message || data.error}`;
303
307
  }
304
308
 
305
309
  const spec = data.spec_content || '';
306
-
307
- // Build header like Circuit UI exports
308
- // Format: # Title
309
- // Priority: #X | Mentions: Y | Type: Z
310
- // ---
311
- // {spec content}
312
-
313
310
  let output = '';
314
311
 
315
- // Status line
316
- const statusText = {
317
- 'ready': 'Ready',
318
- 'building': 'Building',
319
- 'done': 'Done'
320
- }[data.status] || data.status;
312
+ const title = data.title || 'Engineering Brief';
313
+ output += `# ${title}\n\n`;
321
314
 
322
- output += `Status: ${statusText}\n\n`;
315
+ const statusText = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[data.status] || data.status;
316
+ const versionBadge = data.version_badge ? ` · ${data.version_badge}` : '';
317
+ output += `Status: ${statusText}${versionBadge}\n`;
318
+
319
+ if (data.customer_context) {
320
+ const ctx = data.customer_context;
321
+ output += `${ctx.category} · ${ctx.volume} users · ${ctx.paying_percent}% paying\n`;
322
+ }
323
+ output += '\n';
323
324
 
324
- // The spec_content should already be formatted markdown
325
- // Clean up any XML-style tags to proper headers
326
325
  let cleanSpec = spec
327
326
  .replace(/<what_to_build>/gi, '## WHAT TO BUILD\n')
328
327
  .replace(/<\/what_to_build>/gi, '\n')
@@ -337,6 +336,19 @@ function formatBrief(data) {
337
336
 
338
337
  output += cleanSpec;
339
338
 
339
+ if (data.related_memory && data.related_memory.length > 0) {
340
+ output += '\n## WHAT CIRCUIT REMEMBERS\n\n';
341
+ for (const mem of data.related_memory) {
342
+ if (mem.type === 'ship') {
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
+ }
349
+ }
350
+ }
351
+
340
352
  output += `\n---\n`;
341
353
  output += `build_id: \`${data.build_id}\``;
342
354
 
@@ -344,103 +356,76 @@ function formatBrief(data) {
344
356
  }
345
357
 
346
358
  /**
347
- * Format search results for display
348
- */
349
- function formatSearchResults(data) {
350
- if (data.message) {
351
- return data.message;
352
- }
353
-
354
- if (!data.results || data.results.length === 0) {
355
- return 'No feedback found matching your search.';
356
- }
357
-
358
- const lines = [`**${data.total} results found**\n`];
359
-
360
- for (const f of data.results) {
361
- lines.push(`**${f.source}** · Urgency: ${f.urgency}/5`);
362
- lines.push(`> "${f.text}"`);
363
- lines.push('');
364
- }
365
-
366
- return lines.join('\n');
367
- }
368
-
369
- /**
370
- * Format status change response
359
+ * Format act response
371
360
  */
372
- function formatStatusChange(data) {
361
+ function formatAct(data) {
373
362
  if (data.error) {
374
363
  return `Error: ${data.error}`;
375
364
  }
376
365
 
377
366
  if (data.success) {
378
- return data.message;
367
+ let output = data.message;
368
+ if (data.memory_created) {
369
+ output += '\nShip memory recorded — Circuit will remember this for future briefs.';
370
+ }
371
+ return output;
379
372
  }
380
373
 
381
374
  return JSON.stringify(data, null, 2);
382
375
  }
383
376
 
384
377
  /**
385
- * Format insights for display
378
+ * Format ask response
386
379
  */
387
- function formatInsights(data) {
380
+ function formatAsk(data) {
388
381
  if (data.message) {
389
382
  return data.message;
390
383
  }
391
384
 
392
385
  const lines = [];
393
386
 
394
- lines.push(`## Insights from ${data.analyzed} priorities\n`);
395
- lines.push(`**Total feedback volume:** ${data.total_feedback_volume} mentions\n`);
396
-
397
- // Category breakdown
398
- if (data.category_breakdown) {
399
- lines.push('**Category breakdown:**');
400
- for (const [cat, count] of Object.entries(data.category_breakdown)) {
401
- lines.push(`- ${cat}: ${count}`);
387
+ if (data.help_articles && data.help_articles.length > 0) {
388
+ lines.push('**Help Articles:**');
389
+ for (const a of data.help_articles) {
390
+ lines.push(`- **${a.title}**: ${a.content}`);
402
391
  }
403
392
  lines.push('');
404
393
  }
405
394
 
406
- // Top 3 by volume
407
- if (data.top_3_by_volume && data.top_3_by_volume.length > 0) {
408
- lines.push('**Top priorities by volume:**');
409
- for (const p of data.top_3_by_volume) {
410
- lines.push(`- ${p.theme} (${p.volume} users, ${p.category})`);
395
+ if (data.priorities && data.priorities.length > 0) {
396
+ lines.push('**Related Priorities:**');
397
+ for (const p of data.priorities) {
398
+ lines.push(`- ${p.theme} (${p.category}, ${p.volume} users) — priority_id: \`${p.id}\``);
411
399
  }
412
400
  lines.push('');
413
401
  }
414
402
 
415
- // Trends
416
- if (data.trending_up && data.trending_up.length > 0) {
417
- lines.push(`**Trending up:** ${data.trending_up.join(', ')}`);
418
- }
419
- if (data.trending_down && data.trending_down.length > 0) {
420
- lines.push(`**Trending down:** ${data.trending_down.join(', ')}`);
421
- }
422
-
423
- // High urgency
424
- if (data.high_urgency && data.high_urgency.length > 0) {
425
- lines.push('\n**High urgency items:**');
426
- for (const h of data.high_urgency) {
427
- lines.push(`- ${h.theme} (urgency: ${h.urgency})`);
403
+ if (data.feedback && data.feedback.length > 0) {
404
+ lines.push('**Related Feedback:**');
405
+ for (const f of data.feedback) {
406
+ lines.push(`- [${f.source}] "${f.text}"`);
428
407
  }
408
+ lines.push('');
429
409
  }
430
410
 
431
- // Recommendations
432
- if (data.recommendations && data.recommendations.length > 0) {
433
- lines.push('\n**Recommendations:**');
434
- for (const r of data.recommendations) {
435
- lines.push(`- ${r}`);
411
+ if (data.your_patterns) {
412
+ const pat = data.your_patterns;
413
+ lines.push('**Your Patterns:**');
414
+ lines.push(`- Ships: ${pat.ships_count}`);
415
+ lines.push(`- Segment: ${pat.segment_affinity}`);
416
+ if (pat.top_categories) {
417
+ const cats = Object.entries(pat.top_categories).map(([k, v]) => `${k} (${Math.round(v * 100)}%)`).join(', ');
418
+ lines.push(`- Top categories: ${cats}`);
436
419
  }
420
+ lines.push('');
437
421
  }
438
422
 
439
- lines.push('\n---');
440
- if (data.circuit_url) {
441
- lines.push(`View full details: ${data.circuit_url}`);
423
+ if (lines.length === 0) {
424
+ return `No results found for "${data.question}". Try rephrasing.`;
442
425
  }
443
426
 
427
+ lines.push(`---\n${data.total} results found`);
428
+
444
429
  return lines.join('\n');
445
430
  }
446
431
 
@@ -451,28 +436,22 @@ async function handleToolCall(id, params, token) {
451
436
  const { name, arguments: args } = params;
452
437
 
453
438
  try {
454
- // Call the Circuit MCP backend
455
439
  const result = await callMcpApi(token, name, args || {});
456
440
 
457
- // Format response based on tool type
458
441
  let formattedText;
459
442
 
460
443
  switch (name) {
461
- case 'get_priorities':
444
+ case 'circuit.priorities':
462
445
  formattedText = formatPriorities(result);
463
446
  break;
464
- case 'get_brief':
447
+ case 'circuit.brief':
465
448
  formattedText = formatBrief(result);
466
449
  break;
467
- case 'search_feedback':
468
- formattedText = formatSearchResults(result);
450
+ case 'circuit.act':
451
+ formattedText = formatAct(result);
469
452
  break;
470
- case 'start_building':
471
- case 'mark_done':
472
- formattedText = formatStatusChange(result);
473
- break;
474
- case 'get_insights':
475
- formattedText = formatInsights(result);
453
+ case 'circuit.ask':
454
+ formattedText = formatAsk(result);
476
455
  break;
477
456
  default:
478
457
  formattedText = JSON.stringify(result, null, 2);
@@ -494,4 +473,3 @@ async function handleToolCall(id, params, token) {
494
473
  };
495
474
  }
496
475
  }
497
-