@damper/mcp 0.3.2 β†’ 0.3.4

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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Response formatters for MCP tools
3
+ *
4
+ * Extracted for testability - these pure functions format API responses
5
+ * into user-friendly text output.
6
+ */
7
+ export interface StartTaskResult {
8
+ id: string;
9
+ status: string;
10
+ message: string;
11
+ context?: {
12
+ isEmpty: boolean;
13
+ index: Array<{
14
+ section: string;
15
+ preview: string;
16
+ updatedAt: string;
17
+ }>;
18
+ relevantSections?: string[];
19
+ hint?: string;
20
+ };
21
+ }
22
+ export interface CompleteTaskResult {
23
+ id: string;
24
+ status: string;
25
+ documentation?: {
26
+ hasContext: boolean;
27
+ affectedSections: string[];
28
+ reminder: string;
29
+ };
30
+ }
31
+ export interface AbandonTaskResult {
32
+ id: string;
33
+ status: string;
34
+ message: string;
35
+ }
36
+ /**
37
+ * Format start_task response
38
+ */
39
+ export declare function formatStartTaskResponse(result: StartTaskResult): string;
40
+ /**
41
+ * Format complete_task response
42
+ */
43
+ export declare function formatCompleteTaskResponse(result: CompleteTaskResult): string;
44
+ /**
45
+ * Format abandon_task response
46
+ */
47
+ export declare function formatAbandonTaskResponse(result: AbandonTaskResult, hasSummary: boolean): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Response formatters for MCP tools
3
+ *
4
+ * Extracted for testability - these pure functions format API responses
5
+ * into user-friendly text output.
6
+ */
7
+ /**
8
+ * Format start_task response
9
+ */
10
+ export function formatStartTaskResponse(result) {
11
+ const lines = [`Started ${result.id}: ${result.message}`];
12
+ if (result.context) {
13
+ if (result.context.isEmpty) {
14
+ lines.push(`\nπŸ“š ${result.context.hint || 'No project context available.'}`);
15
+ }
16
+ else {
17
+ lines.push('\n**Project context available:**');
18
+ for (const s of result.context.index) {
19
+ const relevant = result.context.relevantSections?.includes(s.section) ? ' ⭐' : '';
20
+ lines.push(`β€’ ${s.section}${relevant}`);
21
+ }
22
+ if (result.context.relevantSections && result.context.relevantSections.length > 0) {
23
+ lines.push(`\nRelevant for this task: ${result.context.relevantSections.join(', ')}`);
24
+ lines.push('Use get_context_section to fetch full content.');
25
+ }
26
+ }
27
+ }
28
+ lines.push('\n---');
29
+ lines.push('**πŸ“‹ Workflow:**');
30
+ lines.push('1. `add_note`: "Session started: <goal>"');
31
+ lines.push('2. Work + log commits/decisions with `add_note`');
32
+ lines.push('3. `add_note`: "Session end: <summary, next steps>"');
33
+ lines.push('4. `complete_task` or `abandon_task`');
34
+ return lines.join('\n');
35
+ }
36
+ /**
37
+ * Format complete_task response
38
+ */
39
+ export function formatCompleteTaskResponse(result) {
40
+ const lines = [`βœ… Completed ${result.id}`];
41
+ if (result.documentation) {
42
+ lines.push('\n---');
43
+ lines.push('**πŸ“š Documentation:**');
44
+ if (result.documentation.affectedSections?.length > 0) {
45
+ lines.push(`May need updates: ${result.documentation.affectedSections.join(', ')}`);
46
+ }
47
+ if (result.documentation.reminder) {
48
+ lines.push(result.documentation.reminder);
49
+ }
50
+ }
51
+ return lines.join('\n');
52
+ }
53
+ /**
54
+ * Format abandon_task response
55
+ */
56
+ export function formatAbandonTaskResponse(result, hasSummary) {
57
+ const lines = [`⏸️ Abandoned ${result.id}: ${result.message}`];
58
+ if (!hasSummary) {
59
+ lines.push('\n⚠️ Tip: Include a summary next time for better handoff.');
60
+ }
61
+ lines.push('\nπŸ’‘ Task returned to "planned". Notes preserved for next agent.');
62
+ return lines.join('\n');
63
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for MCP response formatters
3
+ */
4
+ export {};
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Tests for MCP response formatters
3
+ */
4
+ import { describe, it, expect } from 'bun:test';
5
+ import { formatStartTaskResponse, formatCompleteTaskResponse, formatAbandonTaskResponse, } from './formatters';
6
+ describe('formatStartTaskResponse', () => {
7
+ it('includes workflow steps', () => {
8
+ const result = formatStartTaskResponse({
9
+ id: 'TASK-1',
10
+ status: 'in_progress',
11
+ message: 'Task started',
12
+ });
13
+ expect(result).toContain('**πŸ“‹ Workflow:**');
14
+ expect(result).toContain('`add_note`: "Session started: <goal>"');
15
+ expect(result).toContain('`add_note`: "Session end: <summary, next steps>"');
16
+ expect(result).toContain('`complete_task` or `abandon_task`');
17
+ });
18
+ it('shows empty context hint when context is empty', () => {
19
+ const result = formatStartTaskResponse({
20
+ id: 'TASK-1',
21
+ status: 'in_progress',
22
+ message: 'Task started',
23
+ context: {
24
+ isEmpty: true,
25
+ index: [],
26
+ hint: 'No docs available yet',
27
+ },
28
+ });
29
+ expect(result).toContain('πŸ“š No docs available yet');
30
+ });
31
+ it('lists context sections with star for relevant ones', () => {
32
+ const result = formatStartTaskResponse({
33
+ id: 'TASK-1',
34
+ status: 'in_progress',
35
+ message: 'Task started',
36
+ context: {
37
+ isEmpty: false,
38
+ index: [
39
+ { section: 'overview', preview: 'Project overview', updatedAt: '2024-01-01' },
40
+ { section: 'api', preview: 'API docs', updatedAt: '2024-01-01' },
41
+ { section: 'testing', preview: 'Testing guide', updatedAt: '2024-01-01' },
42
+ ],
43
+ relevantSections: ['api', 'testing'],
44
+ },
45
+ });
46
+ expect(result).toContain('**Project context available:**');
47
+ expect(result).toContain('β€’ overview');
48
+ expect(result).toContain('β€’ api ⭐');
49
+ expect(result).toContain('β€’ testing ⭐');
50
+ expect(result).toContain('Relevant for this task: api, testing');
51
+ expect(result).toContain('Use get_context_section to fetch full content.');
52
+ });
53
+ it('shows basic message and task ID', () => {
54
+ const result = formatStartTaskResponse({
55
+ id: 'TASK-123',
56
+ status: 'in_progress',
57
+ message: 'Locked successfully',
58
+ });
59
+ expect(result).toContain('Started TASK-123: Locked successfully');
60
+ });
61
+ });
62
+ describe('formatCompleteTaskResponse', () => {
63
+ it('shows completion message', () => {
64
+ const result = formatCompleteTaskResponse({
65
+ id: 'TASK-1',
66
+ status: 'done',
67
+ });
68
+ expect(result).toContain('βœ… Completed TASK-1');
69
+ });
70
+ it('shows documentation section with affected sections', () => {
71
+ const result = formatCompleteTaskResponse({
72
+ id: 'TASK-1',
73
+ status: 'done',
74
+ documentation: {
75
+ hasContext: true,
76
+ affectedSections: ['api', 'overview'],
77
+ reminder: 'Please review and update docs',
78
+ },
79
+ });
80
+ expect(result).toContain('**πŸ“š Documentation:**');
81
+ expect(result).toContain('May need updates: api, overview');
82
+ expect(result).toContain('Please review and update docs');
83
+ });
84
+ it('skips affected sections line when empty', () => {
85
+ const result = formatCompleteTaskResponse({
86
+ id: 'TASK-1',
87
+ status: 'done',
88
+ documentation: {
89
+ hasContext: true,
90
+ affectedSections: [],
91
+ reminder: 'No changes needed',
92
+ },
93
+ });
94
+ expect(result).toContain('**πŸ“š Documentation:**');
95
+ expect(result).not.toContain('May need updates:');
96
+ expect(result).toContain('No changes needed');
97
+ });
98
+ it('handles missing documentation gracefully', () => {
99
+ const result = formatCompleteTaskResponse({
100
+ id: 'TASK-1',
101
+ status: 'done',
102
+ });
103
+ expect(result).not.toContain('**πŸ“š Documentation:**');
104
+ expect(result).toBe('βœ… Completed TASK-1');
105
+ });
106
+ });
107
+ describe('formatAbandonTaskResponse', () => {
108
+ it('shows tip when no summary provided', () => {
109
+ const result = formatAbandonTaskResponse({ id: 'TASK-1', status: 'planned', message: 'Task abandoned' }, false);
110
+ expect(result).toContain('⏸️ Abandoned TASK-1: Task abandoned');
111
+ expect(result).toContain('⚠️ Tip: Include a summary next time for better handoff.');
112
+ expect(result).toContain('πŸ’‘ Task returned to "planned". Notes preserved for next agent.');
113
+ });
114
+ it('does not show tip when summary provided', () => {
115
+ const result = formatAbandonTaskResponse({ id: 'TASK-1', status: 'planned', message: 'Task abandoned with handoff' }, true);
116
+ expect(result).toContain('⏸️ Abandoned TASK-1');
117
+ expect(result).not.toContain('⚠️ Tip:');
118
+ expect(result).toContain('πŸ’‘ Task returned to "planned"');
119
+ });
120
+ it('always shows notes preserved message', () => {
121
+ const withSummary = formatAbandonTaskResponse({ id: 'TASK-1', status: 'planned', message: 'Done' }, true);
122
+ const withoutSummary = formatAbandonTaskResponse({ id: 'TASK-1', status: 'planned', message: 'Done' }, false);
123
+ expect(withSummary).toContain('Notes preserved for next agent');
124
+ expect(withoutSummary).toContain('Notes preserved for next agent');
125
+ });
126
+ });
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
+ import { formatStartTaskResponse, formatCompleteTaskResponse, formatAbandonTaskResponse, } from './formatters.js';
5
6
  // Config
6
7
  const API_KEY = process.env.DAMPER_API_KEY;
7
8
  const API_URL = process.env.DAMPER_API_URL || 'https://api.usedamper.com';
@@ -331,8 +332,12 @@ server.registerTool('start_task', {
331
332
  title: 'Start Task',
332
333
  description: 'Lock and start a task. Fails if locked by another agent unless force=true. ' +
333
334
  'Use force=true to take over a task from another agent (e.g., if they abandoned it). ' +
334
- 'Returns project context index with relevant sections for the task. ' +
335
- 'Use add_note for session start/end and commits.',
335
+ 'Returns project context index with relevant sections for the task.\n\n' +
336
+ '**Workflow after starting:**\n' +
337
+ '1. add_note: "Session started: <your goal>"\n' +
338
+ '2. Do work, log commits/decisions with add_note\n' +
339
+ '3. add_note: "Session end: <summary, next steps>"\n' +
340
+ '4. complete_task (done) or abandon_task (stopping)',
336
341
  inputSchema: z.object({
337
342
  taskId: z.string(),
338
343
  force: z.boolean().optional().describe('Take over lock from another agent'),
@@ -354,27 +359,8 @@ server.registerTool('start_task', {
354
359
  }, async ({ taskId, force }) => {
355
360
  try {
356
361
  const result = await api('POST', `/api/agent/tasks/${taskId}/start`, force ? { force: true } : undefined);
357
- // Build response text
358
- const lines = [`Started ${result.id}: ${result.message}`];
359
- if (result.context) {
360
- if (result.context.isEmpty) {
361
- lines.push(`\nπŸ“š ${result.context.hint || 'No project context available.'}`);
362
- }
363
- else {
364
- lines.push('\n**Project context available:**');
365
- for (const s of result.context.index) {
366
- const relevant = result.context.relevantSections?.includes(s.section) ? ' ⭐' : '';
367
- lines.push(`β€’ ${s.section}${relevant}`);
368
- }
369
- if (result.context.relevantSections && result.context.relevantSections.length > 0) {
370
- lines.push(`\nRelevant for this task: ${result.context.relevantSections.join(', ')}`);
371
- lines.push('Use get_context_section to fetch full content.');
372
- }
373
- }
374
- }
375
- lines.push('\nπŸ’‘ Use add_note to log session start, commits, and session end with next steps.');
376
362
  return {
377
- content: [{ type: 'text', text: lines.join('\n') }],
363
+ content: [{ type: 'text', text: formatStartTaskResponse(result) }],
378
364
  structuredContent: result,
379
365
  };
380
366
  }
@@ -406,8 +392,13 @@ server.registerTool('start_task', {
406
392
  // Tool: Add note
407
393
  server.registerTool('add_note', {
408
394
  title: 'Add Note',
409
- description: 'Add progress note to task. Log session start, commits (with hashes), decisions, ' +
410
- 'and session end with next steps. Notes help future agents continue your work.',
395
+ description: 'Add progress note to task. Notes help future agents continue your work.\n\n' +
396
+ '**When to use:**\n' +
397
+ '- Session start: "Session started: implementing X"\n' +
398
+ '- Commits: "Committed abc123: Added validation"\n' +
399
+ '- Decisions: "Decision: Using X because Y"\n' +
400
+ '- Session end: "Session end: Done X, remaining Y, blockers Z"\n\n' +
401
+ '**Important:** Always log session end before complete_task or abandon_task.',
411
402
  inputSchema: z.object({
412
403
  taskId: z.string(),
413
404
  note: z.string(),
@@ -509,7 +500,11 @@ const DocumentationSchema = z.object({
509
500
  // Tool: Complete task
510
501
  server.registerTool('complete_task', {
511
502
  title: 'Complete Task',
512
- description: 'Mark task done with summary. Include commit hashes and any follow-up work. ' +
503
+ description: 'Mark task done with summary. Include commit hashes and any follow-up work.\n\n' +
504
+ '**Before calling:**\n' +
505
+ '1. Push all commits\n' +
506
+ '2. add_note: "Session end: <what was done>"\n' +
507
+ '3. Check if project context docs need updating\n\n' +
513
508
  'Returns documentation update suggestions.',
514
509
  inputSchema: z.object({
515
510
  taskId: z.string(),
@@ -528,20 +523,19 @@ server.registerTool('complete_task', {
528
523
  },
529
524
  }, async ({ taskId, summary }) => {
530
525
  const result = await api('POST', `/api/agent/tasks/${taskId}/complete`, { summary });
531
- const lines = [`βœ… Completed ${result.id}`];
532
- if (result.documentation?.reminder) {
533
- lines.push(`\nπŸ“ ${result.documentation.reminder}`);
534
- }
535
526
  return {
536
- content: [{ type: 'text', text: lines.join('\n') }],
527
+ content: [{ type: 'text', text: formatCompleteTaskResponse(result) }],
537
528
  structuredContent: result,
538
529
  };
539
530
  });
540
531
  // Tool: Abandon task
541
532
  server.registerTool('abandon_task', {
542
533
  title: 'Abandon Task',
543
- description: 'Release lock and return task to planned status. Use when you cannot complete the task ' +
544
- 'or need to stop working on it. Optionally provide summary (what done, what remains, blockers) for handoff.',
534
+ description: 'Release lock and return task to planned status. Use when stopping work.\n\n' +
535
+ '**Before calling:**\n' +
536
+ '1. Push any WIP commits\n' +
537
+ '2. add_note: "Session end: <progress, blockers, next steps>"\n\n' +
538
+ '**Summary parameter:** What was done, what remains, blockers. Helps the next agent.',
545
539
  inputSchema: z.object({
546
540
  taskId: z.string(),
547
541
  summary: z.string().optional().describe('Handoff summary: what was done, what remains, any blockers'),
@@ -560,7 +554,7 @@ server.registerTool('abandon_task', {
560
554
  }, async ({ taskId, summary }) => {
561
555
  const result = await api('POST', `/api/agent/tasks/${taskId}/abandon`, summary ? { summary } : undefined);
562
556
  return {
563
- content: [{ type: 'text', text: `Abandoned ${result.id}: ${result.message}` }],
557
+ content: [{ type: 'text', text: formatAbandonTaskResponse(result, !!summary) }],
564
558
  structuredContent: result,
565
559
  };
566
560
  });
@@ -877,6 +871,49 @@ server.registerTool('get_feedback', {
877
871
  structuredContent: f,
878
872
  };
879
873
  });
874
+ // ==================== Issue Reporting ====================
875
+ // Tool: Report issue
876
+ server.registerTool('report_issue', {
877
+ title: 'Report Issue',
878
+ description: 'Report an error, bug, or feature request. Use when encountering issues with MCP tools or to suggest improvements.',
879
+ inputSchema: z.object({
880
+ category: z.enum(['error', 'feature_request', 'improvement']).describe('Type of report'),
881
+ title: z.string().describe('Brief summary of the issue'),
882
+ description: z.string().describe('Detailed description'),
883
+ context: z.object({
884
+ toolName: z.string().optional().describe('MCP tool that encountered the issue'),
885
+ errorMessage: z.string().optional().describe('Error message if applicable'),
886
+ taskId: z.string().optional().describe('Related task ID if applicable'),
887
+ }).optional(),
888
+ }),
889
+ outputSchema: z.object({
890
+ id: z.string(),
891
+ title: z.string(),
892
+ type: z.string(),
893
+ status: z.string(),
894
+ isDuplicate: z.boolean(),
895
+ existingIssueId: z.string().nullable(),
896
+ }),
897
+ annotations: {
898
+ readOnlyHint: false,
899
+ destructiveHint: false,
900
+ idempotentHint: false,
901
+ openWorldHint: false,
902
+ },
903
+ }, async ({ category, title, description, context }) => {
904
+ const result = await api('POST', '/api/agent/reports', { category, title, description, context });
905
+ const typeIcon = result.type === 'bug' ? 'πŸ›' : result.type === 'feature' ? '✨' : 'πŸ’‘';
906
+ if (result.isDuplicate) {
907
+ return {
908
+ content: [{ type: 'text', text: `⚠️ Similar issue already reported: ${result.existingIssueId} ${typeIcon} "${result.title}"` }],
909
+ structuredContent: result,
910
+ };
911
+ }
912
+ return {
913
+ content: [{ type: 'text', text: `βœ… Reported: ${result.id} ${typeIcon} "${result.title}" [${result.status}]` }],
914
+ structuredContent: result,
915
+ };
916
+ });
880
917
  // ==================== Template Tools ====================
881
918
  // Tool: List templates
882
919
  server.registerTool('list_templates', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "MCP server for Damper task management",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.25.0",
28
- "zod": "^3.24.0"
28
+ "zod": "^3.25.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.0.0",