circuit-mcp 1.0.13 → 1.0.14

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 +176 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuit-mcp",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Circuit MCP server for Cursor and Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
package/src/server.js CHANGED
@@ -204,6 +204,160 @@ async function handleMessage(message, token) {
204
204
  }
205
205
  }
206
206
 
207
+ /**
208
+ * Format priorities for display (matches Circuit UI exactly)
209
+ */
210
+ function formatPriorities(data) {
211
+ if (data.message) {
212
+ return data.message;
213
+ }
214
+
215
+ if (!data.priorities || data.priorities.length === 0) {
216
+ return 'No priorities found. Upload feedback to Circuit to get started.';
217
+ }
218
+
219
+ const lines = [];
220
+
221
+ for (const p of data.priorities) {
222
+ // Format trend indicator like Circuit UI
223
+ let trendText = '';
224
+ if (p.trend === 'up') {
225
+ trendText = ' ↑';
226
+ } else if (p.trend === 'down') {
227
+ trendText = ' ↓';
228
+ }
229
+
230
+ // Format: #Rank. Title
231
+ lines.push(`**#${p.rank}. ${p.theme}**`);
232
+
233
+ // Badges row: Category | X users | Trend
234
+ const badges = [];
235
+ badges.push(p.category || 'Other');
236
+ badges.push(`${p.volume} users`);
237
+ if (trendText) badges.push(trendText.trim());
238
+
239
+ lines.push(badges.join(' · '));
240
+
241
+ // Key quote (customer voice)
242
+ if (p.key_quote) {
243
+ lines.push(`> "${p.key_quote}"`);
244
+ }
245
+
246
+ lines.push(`priority_id: \`${p.id}\``);
247
+ lines.push('');
248
+ }
249
+
250
+ lines.push('---');
251
+ lines.push('Use `get_brief` with a priority_id to see the full engineering spec.');
252
+
253
+ return lines.join('\n');
254
+ }
255
+
256
+ /**
257
+ * Format brief for display (matches Circuit UI format for Cursor/Claude)
258
+ */
259
+ function formatBrief(data) {
260
+ if (data.error) {
261
+ if (data.error === 'no_brief' && data.priority) {
262
+ const p = data.priority;
263
+ let output = `# ${p.theme || 'Priority'}\n\n`;
264
+ output += `${p.category || 'Other'} · ${p.volume || 0} users\n\n`;
265
+ output += `**No brief generated yet**\n\n`;
266
+
267
+ if (p.summary) {
268
+ output += `${p.summary}\n\n`;
269
+ }
270
+
271
+ if (p.key_quote) {
272
+ output += `> "${p.key_quote}"\n\n`;
273
+ }
274
+
275
+ output += `---\n`;
276
+ output += data.suggestion;
277
+ return output;
278
+ }
279
+ return `Error: ${data.message || data.error}`;
280
+ }
281
+
282
+ const spec = data.spec_content || '';
283
+
284
+ // Build header like Circuit UI exports
285
+ // Format: # Title
286
+ // Priority: #X | Mentions: Y | Type: Z
287
+ // ---
288
+ // {spec content}
289
+
290
+ let output = '';
291
+
292
+ // Status line
293
+ const statusText = {
294
+ 'ready': 'Ready',
295
+ 'building': 'Building',
296
+ 'done': 'Done'
297
+ }[data.status] || data.status;
298
+
299
+ output += `Status: ${statusText}\n\n`;
300
+
301
+ // The spec_content should already be formatted markdown
302
+ // Clean up any XML-style tags to proper headers
303
+ let cleanSpec = spec
304
+ .replace(/<what_to_build>/gi, '## WHAT TO BUILD\n')
305
+ .replace(/<\/what_to_build>/gi, '\n')
306
+ .replace(/<why_it_matters>/gi, '## WHY IT MATTERS\n')
307
+ .replace(/<\/why_it_matters>/gi, '\n')
308
+ .replace(/<customer_voice>/gi, '## CUSTOMER VOICE\n')
309
+ .replace(/<\/customer_voice>/gi, '\n')
310
+ .replace(/<files_to_touch>/gi, '## FILES TO TOUCH\n')
311
+ .replace(/<\/files_to_touch>/gi, '\n')
312
+ .replace(/<done_when>/gi, '## DONE WHEN\n')
313
+ .replace(/<\/done_when>/gi, '\n');
314
+
315
+ output += cleanSpec;
316
+
317
+ output += `\n---\n`;
318
+ output += `build_id: \`${data.build_id}\``;
319
+
320
+ return output;
321
+ }
322
+
323
+ /**
324
+ * Format search results for display
325
+ */
326
+ function formatSearchResults(data) {
327
+ if (data.message) {
328
+ return data.message;
329
+ }
330
+
331
+ if (!data.results || data.results.length === 0) {
332
+ return 'No feedback found matching your search.';
333
+ }
334
+
335
+ const lines = [`**${data.total} results found**\n`];
336
+
337
+ for (const f of data.results) {
338
+ lines.push(`**${f.source}** · Urgency: ${f.urgency}/5`);
339
+ lines.push(`> "${f.text}"`);
340
+ lines.push('');
341
+ }
342
+
343
+ return lines.join('\n');
344
+ }
345
+
346
+ /**
347
+ * Format status change response
348
+ */
349
+ function formatStatusChange(data) {
350
+ if (data.error) {
351
+ return `Error: ${data.error}`;
352
+ }
353
+
354
+ if (data.success) {
355
+ return data.message;
356
+ }
357
+
358
+ return JSON.stringify(data, null, 2);
359
+ }
360
+
207
361
  /**
208
362
  * Handle tool calls
209
363
  */
@@ -214,11 +368,32 @@ async function handleToolCall(id, params, token) {
214
368
  // Call the Circuit MCP backend
215
369
  const result = await callMcpApi(token, name, args || {});
216
370
 
371
+ // Format response based on tool type
372
+ let formattedText;
373
+
374
+ switch (name) {
375
+ case 'get_priorities':
376
+ formattedText = formatPriorities(result);
377
+ break;
378
+ case 'get_brief':
379
+ formattedText = formatBrief(result);
380
+ break;
381
+ case 'search_feedback':
382
+ formattedText = formatSearchResults(result);
383
+ break;
384
+ case 'start_building':
385
+ case 'mark_done':
386
+ formattedText = formatStatusChange(result);
387
+ break;
388
+ default:
389
+ formattedText = JSON.stringify(result, null, 2);
390
+ }
391
+
217
392
  return {
218
393
  jsonrpc: '2.0',
219
394
  id,
220
395
  result: {
221
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
396
+ content: [{ type: 'text', text: formattedText }]
222
397
  }
223
398
  };
224
399