@damper/mcp 0.3.3 β 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.
- package/dist/formatters.d.ts +47 -0
- package/dist/formatters.js +63 -0
- package/dist/formatters.test.d.ts +4 -0
- package/dist/formatters.test.js +126 -0
- package/dist/index.js +27 -33
- package/package.json +1 -1
|
@@ -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,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
|
-
'
|
|
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:
|
|
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.
|
|
410
|
-
'
|
|
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:
|
|
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
|
|
544
|
-
'
|
|
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:
|
|
557
|
+
content: [{ type: 'text', text: formatAbandonTaskResponse(result, !!summary) }],
|
|
564
558
|
structuredContent: result,
|
|
565
559
|
};
|
|
566
560
|
});
|