circuit-mcp 1.0.17 → 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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +147 -185
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuit-mcp",
3
- "version": "1.0.17",
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/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,111 +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_shipped',
147
- description: "Mark a brief as 'shipped' - the feature has shipped! This closes the feedback loop and notifies customers.",
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 shipped'
154
- }
155
- },
156
- required: ['build_id']
157
- }
158
- },
159
- {
160
- name: 'mark_done',
161
- description: "[Deprecated - use mark_shipped] Mark a brief as shipped.",
162
- inputSchema: {
163
- type: 'object',
164
- properties: {
165
- build_id: {
160
+ description: "Brief ID (for 'build' and 'ship' actions)"
161
+ },
162
+ priority_id: {
166
163
  type: 'string',
167
- description: 'The build ID to mark as shipped'
168
- }
169
- },
170
- required: ['build_id']
171
- }
172
- },
173
- {
174
- name: 'search_feedback',
175
- description: 'Search customer feedback by keyword. Useful for understanding pain points around specific features.',
176
- inputSchema: {
177
- type: 'object',
178
- properties: {
179
- query: {
164
+ description: "Priority ID (for 'correct' action)"
165
+ },
166
+ correction_type: {
180
167
  type: 'string',
181
- description: 'Search query (keywords or phrase)'
168
+ description: "What to correct (for 'correct' action): 'category'",
169
+ enum: ['category']
182
170
  },
183
- limit: {
184
- type: 'number',
185
- description: 'Max results (default: 10)'
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'
186
187
  }
187
188
  },
188
- required: ['query']
189
+ required: ['action']
189
190
  }
190
191
  },
191
192
  {
192
- name: 'get_insights',
193
- 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.',
194
195
  inputSchema: {
195
196
  type: 'object',
196
197
  properties: {
197
- limit: {
198
- type: 'number',
199
- 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?')"
200
201
  }
201
- }
202
+ },
203
+ required: ['question']
202
204
  }
203
205
  }
204
206
  ]
@@ -232,7 +234,7 @@ async function handleMessage(message, token) {
232
234
  }
233
235
 
234
236
  /**
235
- * Format priorities for display (matches Circuit UI exactly)
237
+ * Format priorities response
236
238
  */
237
239
  function formatPriorities(data) {
238
240
  if (data.message) {
@@ -245,47 +247,49 @@ function formatPriorities(data) {
245
247
 
246
248
  const lines = [];
247
249
 
250
+ if (data.memory_applied) {
251
+ lines.push(`*Memory active: ${data.ships_count} ships tracked, segment: ${data.segment_affinity || 'mixed'}*\n`);
252
+ }
253
+
248
254
  for (const p of data.priorities) {
249
- // Format trend indicator like Circuit UI
250
255
  let trendText = '';
251
256
  if (p.trend === 'up') {
252
- trendText = ' ';
257
+ trendText = ` ↑${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
253
258
  } else if (p.trend === 'down') {
254
- trendText = ' ';
259
+ trendText = ` ↓${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
255
260
  }
256
261
 
257
- // Format: #Rank. Title
258
262
  lines.push(`**#${p.rank}. ${p.theme}**`);
259
263
 
260
- // Badges row: Category | X users | Trend | Status
261
264
  const badges = [];
262
265
  badges.push(p.category || 'Other');
263
266
  badges.push(`${p.volume} users`);
264
267
  if (trendText) badges.push(trendText.trim());
265
268
  if (p.brief_status && p.brief_status !== 'no_brief') {
266
- const statusLabel = { ready: 'Ready', building: 'Building', shipped: 'Shipped', done: 'Shipped' }[p.brief_status];
269
+ const statusLabel = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[p.brief_status];
267
270
  if (statusLabel) badges.push(statusLabel);
268
271
  }
272
+ if (p.matches_pattern) badges.push('matches pattern');
273
+ if (p.version) badges.push(p.version);
269
274
 
270
275
  lines.push(badges.join(' · '));
271
276
 
272
- // Key quote (customer voice)
273
277
  if (p.key_quote) {
274
278
  lines.push(`> "${p.key_quote}"`);
275
279
  }
276
280
 
277
- lines.push(`priority_id: \`${p.id}\``);
281
+ lines.push(`priority_id: \`${p.priority_id}\`${p.build_id ? ` · build_id: \`${p.build_id}\`` : ''}`);
278
282
  lines.push('');
279
283
  }
280
284
 
281
285
  lines.push('---');
282
- 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.');
283
287
 
284
288
  return lines.join('\n');
285
289
  }
286
290
 
287
291
  /**
288
- * Format brief for display (matches Circuit UI format for Cursor/Claude)
292
+ * Format brief response
289
293
  */
290
294
  function formatBrief(data) {
291
295
  if (data.error) {
@@ -293,51 +297,31 @@ function formatBrief(data) {
293
297
  const p = data.priority;
294
298
  let output = `# ${p.theme || 'Priority'}\n\n`;
295
299
  output += `${p.category || 'Other'} · ${p.volume || 0} users\n\n`;
296
- output += `**No brief generated yet**\n\n`;
297
-
298
- if (p.summary) {
299
- output += `${p.summary}\n\n`;
300
+ output += `**No brief generated yet.**\n\n`;
301
+ if (data.suggestion) {
302
+ output += `${data.suggestion}\n`;
300
303
  }
301
-
302
- if (p.key_quote) {
303
- output += `> "${p.key_quote}"\n\n`;
304
- }
305
-
306
- output += `---\n\n`;
307
-
308
- // Include deep link to Circuit
309
- if (data.circuit_url) {
310
- output += `**Generate brief:** ${data.circuit_url}\n\n`;
311
- }
312
-
313
- output += `Or I can help you draft implementation notes based on the customer feedback above.`;
314
304
  return output;
315
305
  }
316
306
  return `Error: ${data.message || data.error}`;
317
307
  }
318
308
 
319
309
  const spec = data.spec_content || '';
320
-
321
- // Build header like Circuit UI exports
322
- // Format: # Title
323
- // Priority: #X | Mentions: Y | Type: Z
324
- // ---
325
- // {spec content}
326
-
327
310
  let output = '';
328
311
 
329
- // Status line (handle both 'shipped' and legacy 'done')
330
- const statusText = {
331
- 'ready': 'Ready',
332
- 'building': 'Building',
333
- 'shipped': 'Shipped',
334
- 'done': 'Shipped'
335
- }[data.status] || data.status;
312
+ const title = data.title || 'Engineering Brief';
313
+ output += `# ${title}\n\n`;
314
+
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`;
336
318
 
337
- output += `Status: ${statusText}\n\n`;
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';
338
324
 
339
- // The spec_content should already be formatted markdown
340
- // Clean up any XML-style tags to proper headers
341
325
  let cleanSpec = spec
342
326
  .replace(/<what_to_build>/gi, '## WHAT TO BUILD\n')
343
327
  .replace(/<\/what_to_build>/gi, '\n')
@@ -352,6 +336,19 @@ function formatBrief(data) {
352
336
 
353
337
  output += cleanSpec;
354
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
+
355
352
  output += `\n---\n`;
356
353
  output += `build_id: \`${data.build_id}\``;
357
354
 
@@ -359,103 +356,76 @@ function formatBrief(data) {
359
356
  }
360
357
 
361
358
  /**
362
- * Format search results for display
363
- */
364
- function formatSearchResults(data) {
365
- if (data.message) {
366
- return data.message;
367
- }
368
-
369
- if (!data.results || data.results.length === 0) {
370
- return 'No feedback found matching your search.';
371
- }
372
-
373
- const lines = [`**${data.total} results found**\n`];
374
-
375
- for (const f of data.results) {
376
- lines.push(`**${f.source}** · Urgency: ${f.urgency}/5`);
377
- lines.push(`> "${f.text}"`);
378
- lines.push('');
379
- }
380
-
381
- return lines.join('\n');
382
- }
383
-
384
- /**
385
- * Format status change response
359
+ * Format act response
386
360
  */
387
- function formatStatusChange(data) {
361
+ function formatAct(data) {
388
362
  if (data.error) {
389
363
  return `Error: ${data.error}`;
390
364
  }
391
365
 
392
366
  if (data.success) {
393
- 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;
394
372
  }
395
373
 
396
374
  return JSON.stringify(data, null, 2);
397
375
  }
398
376
 
399
377
  /**
400
- * Format insights for display
378
+ * Format ask response
401
379
  */
402
- function formatInsights(data) {
380
+ function formatAsk(data) {
403
381
  if (data.message) {
404
382
  return data.message;
405
383
  }
406
384
 
407
385
  const lines = [];
408
386
 
409
- lines.push(`## Insights from ${data.analyzed} priorities\n`);
410
- lines.push(`**Total feedback volume:** ${data.total_feedback_volume} mentions\n`);
411
-
412
- // Category breakdown
413
- if (data.category_breakdown) {
414
- lines.push('**Category breakdown:**');
415
- for (const [cat, count] of Object.entries(data.category_breakdown)) {
416
- 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}`);
417
391
  }
418
392
  lines.push('');
419
393
  }
420
394
 
421
- // Top 3 by volume
422
- if (data.top_3_by_volume && data.top_3_by_volume.length > 0) {
423
- lines.push('**Top priorities by volume:**');
424
- for (const p of data.top_3_by_volume) {
425
- 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}\``);
426
399
  }
427
400
  lines.push('');
428
401
  }
429
402
 
430
- // Trends
431
- if (data.trending_up && data.trending_up.length > 0) {
432
- lines.push(`**Trending up:** ${data.trending_up.join(', ')}`);
433
- }
434
- if (data.trending_down && data.trending_down.length > 0) {
435
- lines.push(`**Trending down:** ${data.trending_down.join(', ')}`);
436
- }
437
-
438
- // High urgency
439
- if (data.high_urgency && data.high_urgency.length > 0) {
440
- lines.push('\n**High urgency items:**');
441
- for (const h of data.high_urgency) {
442
- 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}"`);
443
407
  }
408
+ lines.push('');
444
409
  }
445
410
 
446
- // Recommendations
447
- if (data.recommendations && data.recommendations.length > 0) {
448
- lines.push('\n**Recommendations:**');
449
- for (const r of data.recommendations) {
450
- 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}`);
451
419
  }
420
+ lines.push('');
452
421
  }
453
422
 
454
- lines.push('\n---');
455
- if (data.circuit_url) {
456
- lines.push(`View full details: ${data.circuit_url}`);
423
+ if (lines.length === 0) {
424
+ return `No results found for "${data.question}". Try rephrasing.`;
457
425
  }
458
426
 
427
+ lines.push(`---\n${data.total} results found`);
428
+
459
429
  return lines.join('\n');
460
430
  }
461
431
 
@@ -466,29 +436,22 @@ async function handleToolCall(id, params, token) {
466
436
  const { name, arguments: args } = params;
467
437
 
468
438
  try {
469
- // Call the Circuit MCP backend
470
439
  const result = await callMcpApi(token, name, args || {});
471
440
 
472
- // Format response based on tool type
473
441
  let formattedText;
474
442
 
475
443
  switch (name) {
476
- case 'get_priorities':
444
+ case 'circuit.priorities':
477
445
  formattedText = formatPriorities(result);
478
446
  break;
479
- case 'get_brief':
447
+ case 'circuit.brief':
480
448
  formattedText = formatBrief(result);
481
449
  break;
482
- case 'search_feedback':
483
- formattedText = formatSearchResults(result);
450
+ case 'circuit.act':
451
+ formattedText = formatAct(result);
484
452
  break;
485
- case 'start_building':
486
- case 'mark_shipped':
487
- case 'mark_done':
488
- formattedText = formatStatusChange(result);
489
- break;
490
- case 'get_insights':
491
- formattedText = formatInsights(result);
453
+ case 'circuit.ask':
454
+ formattedText = formatAsk(result);
492
455
  break;
493
456
  default:
494
457
  formattedText = JSON.stringify(result, null, 2);
@@ -510,4 +473,3 @@ async function handleToolCall(id, params, token) {
510
473
  };
511
474
  }
512
475
  }
513
-