@damper/mcp 0.1.1 → 0.1.3

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/dist/index.js +234 -18
  2. package/package.json +10 -3
package/dist/index.js CHANGED
@@ -21,6 +21,12 @@ async function api(method, path, body) {
21
21
  });
22
22
  if (!res.ok) {
23
23
  const err = await res.json().catch(() => ({}));
24
+ // Preserve lock info for 409 conflicts
25
+ if (res.status === 409 && err.lockedBy) {
26
+ const lockErr = new Error(err.error || `HTTP ${res.status}`);
27
+ lockErr.lockInfo = err;
28
+ throw lockErr;
29
+ }
24
30
  throw new Error(err.error || `HTTP ${res.status}`);
25
31
  }
26
32
  return res.json();
@@ -30,31 +36,94 @@ const server = new McpServer({
30
36
  name: 'damper',
31
37
  version: '0.1.0',
32
38
  });
39
+ // Output schemas
40
+ const TaskSummarySchema = z.object({
41
+ id: z.string(),
42
+ title: z.string(),
43
+ type: z.string(),
44
+ status: z.string(),
45
+ priority: z.string(),
46
+ feedbackCount: z.number(),
47
+ hasImplementationPlan: z.boolean(),
48
+ });
49
+ const TaskDetailSchema = z.object({
50
+ id: z.string(),
51
+ title: z.string(),
52
+ description: z.string().optional(),
53
+ type: z.string(),
54
+ implementationPlan: z.string().optional(),
55
+ status: z.string(),
56
+ voteScore: z.number(),
57
+ agentNotes: z.string().optional(),
58
+ feedback: z.array(z.object({
59
+ id: z.string(),
60
+ title: z.string(),
61
+ description: z.string(),
62
+ voterCount: z.number(),
63
+ })),
64
+ });
65
+ const FeedbackSummarySchema = z.object({
66
+ id: z.string(),
67
+ title: z.string(),
68
+ type: z.string(),
69
+ voterCount: z.number(),
70
+ linkedTaskId: z.string().optional(),
71
+ });
72
+ const FeedbackDetailSchema = z.object({
73
+ id: z.string(),
74
+ title: z.string(),
75
+ description: z.string(),
76
+ type: z.string(),
77
+ status: z.string(),
78
+ voteScore: z.number(),
79
+ linkedTaskId: z.string().optional(),
80
+ voters: z.array(z.object({ email: z.string(), plan: z.string() })),
81
+ comments: z.array(z.object({ author: z.string(), body: z.string() })),
82
+ });
33
83
  // Tool: List tasks
34
84
  server.registerTool('list_tasks', {
35
85
  title: 'List Tasks',
36
- description: 'Get roadmap tasks. Returns planned/in-progress by default.',
86
+ description: 'Get roadmap tasks. Returns planned/in-progress by default. ' +
87
+ 'Filter by type to get bugs, features, etc.',
37
88
  inputSchema: z.object({
38
89
  status: z.enum(['planned', 'in_progress', 'done', 'all']).optional(),
90
+ type: z.enum(['bug', 'feature', 'improvement', 'task']).optional().describe('Filter by task type'),
39
91
  limit: z.number().optional(),
40
92
  }),
41
- }, async ({ status, limit }) => {
93
+ outputSchema: z.object({
94
+ project: z.string(),
95
+ tasks: z.array(TaskSummarySchema),
96
+ }),
97
+ annotations: {
98
+ readOnlyHint: true,
99
+ destructiveHint: false,
100
+ idempotentHint: true,
101
+ openWorldHint: false,
102
+ },
103
+ }, async ({ status, type, limit }) => {
42
104
  const params = new URLSearchParams();
43
105
  if (status)
44
106
  params.set('status', status);
107
+ if (type)
108
+ params.set('type', type);
45
109
  if (limit)
46
110
  params.set('limit', String(limit));
47
111
  const query = params.toString();
48
112
  const data = await api('GET', `/api/agent/tasks${query ? `?${query}` : ''}`);
49
113
  if (!data.tasks.length) {
50
- return { content: [{ type: 'text', text: `No tasks in "${data.project.name}"` }] };
114
+ return {
115
+ content: [{ type: 'text', text: `No tasks in "${data.project.name}"${type ? ` (type: ${type})` : ''}` }],
116
+ structuredContent: { project: data.project.name, tasks: [] },
117
+ };
51
118
  }
52
119
  const lines = data.tasks.map((t) => {
53
120
  const p = t.priority === 'high' ? '🔴' : t.priority === 'medium' ? '🟡' : '⚪';
54
- return `• ${t.id}: ${t.title} [${t.status}] ${p}${t.hasImplementationPlan ? ' 📋' : ''}`;
121
+ const typeIcon = t.type === 'bug' ? '🐛' : t.type === 'feature' ? '✨' : t.type === 'improvement' ? '💡' : '📌';
122
+ return `• ${t.id}: ${typeIcon} ${t.title} [${t.status}] ${p}${t.hasImplementationPlan ? ' 📋' : ''}`;
55
123
  });
56
124
  return {
57
125
  content: [{ type: 'text', text: `Tasks in "${data.project.name}":\n${lines.join('\n')}` }],
126
+ structuredContent: { project: data.project.name, tasks: data.tasks },
58
127
  };
59
128
  });
60
129
  // Tool: Get task
@@ -64,11 +133,19 @@ server.registerTool('get_task', {
64
133
  inputSchema: z.object({
65
134
  taskId: z.string().describe('Task ID'),
66
135
  }),
136
+ outputSchema: TaskDetailSchema,
137
+ annotations: {
138
+ readOnlyHint: true,
139
+ destructiveHint: false,
140
+ idempotentHint: true,
141
+ openWorldHint: false,
142
+ },
67
143
  }, async ({ taskId }) => {
68
144
  const t = await api('GET', `/api/agent/tasks/${taskId}`);
145
+ const typeIcon = t.type === 'bug' ? '🐛' : t.type === 'feature' ? '✨' : t.type === 'improvement' ? '💡' : '📌';
69
146
  const parts = [
70
- `# ${t.title}`,
71
- `Status: ${t.status} | Score: ${t.voteScore}`,
147
+ `# ${typeIcon} ${t.title}`,
148
+ `Type: ${t.type} | Status: ${t.status} | Score: ${t.voteScore}`,
72
149
  ];
73
150
  if (t.description)
74
151
  parts.push(`\n## Description\n${t.description}`);
@@ -80,34 +157,96 @@ server.registerTool('get_task', {
80
157
  parts.push(`\n## Feedback (${t.feedback.length})`);
81
158
  t.feedback.forEach((f) => parts.push(`- ${f.title} (${f.voterCount} votes)`));
82
159
  }
83
- return { content: [{ type: 'text', text: parts.join('\n') }] };
160
+ return {
161
+ content: [{ type: 'text', text: parts.join('\n') }],
162
+ structuredContent: t,
163
+ };
84
164
  });
85
165
  // Tool: Create task
86
166
  server.registerTool('create_task', {
87
167
  title: 'Create Task',
88
- description: 'Create a new roadmap task. Use for importing from TODO files.',
168
+ description: 'Create a new roadmap task. Use for importing from TODO files or creating work items.',
89
169
  inputSchema: z.object({
90
170
  title: z.string(),
91
171
  description: z.string().optional(),
172
+ type: z.enum(['bug', 'feature', 'improvement', 'task']).optional().describe('Task type (default: feature)'),
92
173
  status: z.enum(['planned', 'in_progress']).optional(),
93
174
  implementationPlan: z.string().optional(),
94
175
  }),
176
+ outputSchema: z.object({
177
+ id: z.string(),
178
+ title: z.string(),
179
+ type: z.string(),
180
+ status: z.string(),
181
+ }),
182
+ annotations: {
183
+ readOnlyHint: false,
184
+ destructiveHint: false,
185
+ idempotentHint: false,
186
+ openWorldHint: false,
187
+ },
95
188
  }, async (args) => {
96
189
  const result = await api('POST', '/api/agent/tasks', args);
190
+ const typeIcon = result.type === 'bug' ? '🐛' : result.type === 'feature' ? '✨' : result.type === 'improvement' ? '💡' : '📌';
97
191
  return {
98
- content: [{ type: 'text', text: `Created: ${result.id} "${result.title}" [${result.status}]` }],
192
+ content: [{ type: 'text', text: `Created: ${result.id} ${typeIcon} "${result.title}" [${result.status}]` }],
193
+ structuredContent: result,
99
194
  };
100
195
  });
101
196
  // Tool: Start task
102
197
  server.registerTool('start_task', {
103
198
  title: 'Start Task',
104
- description: 'Mark task as in-progress. Call before working on it.',
199
+ description: 'Lock and start a task. Fails if locked by another agent unless force=true. ' +
200
+ 'Use force=true to take over a task from another agent (e.g., if they abandoned it).',
105
201
  inputSchema: z.object({
106
202
  taskId: z.string(),
203
+ force: z.boolean().optional().describe('Take over lock from another agent'),
107
204
  }),
108
- }, async ({ taskId }) => {
109
- const result = await api('POST', `/api/agent/tasks/${taskId}/start`);
110
- return { content: [{ type: 'text', text: `Started ${result.id}: ${result.message}` }] };
205
+ outputSchema: z.object({
206
+ id: z.string(),
207
+ status: z.string(),
208
+ message: z.string(),
209
+ lockedBy: z.string().optional(),
210
+ lockedAt: z.string().optional(),
211
+ }),
212
+ annotations: {
213
+ readOnlyHint: false,
214
+ destructiveHint: false,
215
+ idempotentHint: true,
216
+ openWorldHint: false,
217
+ },
218
+ }, async ({ taskId, force }) => {
219
+ try {
220
+ const result = await api('POST', `/api/agent/tasks/${taskId}/start`, force ? { force: true } : undefined);
221
+ return {
222
+ content: [{ type: 'text', text: `Started ${result.id}: ${result.message}` }],
223
+ structuredContent: result,
224
+ };
225
+ }
226
+ catch (err) {
227
+ const error = err;
228
+ if (error.lockInfo) {
229
+ // Return lock info so AI can decide whether to force takeover
230
+ return {
231
+ content: [
232
+ {
233
+ type: 'text',
234
+ text: `⚠️ Task is locked by "${error.lockInfo.lockedBy}" since ${error.lockInfo.lockedAt}. ` +
235
+ 'Use force=true to take over the lock.',
236
+ },
237
+ ],
238
+ structuredContent: {
239
+ id: taskId,
240
+ status: 'locked',
241
+ message: error.message,
242
+ lockedBy: error.lockInfo.lockedBy,
243
+ lockedAt: error.lockInfo.lockedAt,
244
+ },
245
+ isError: true,
246
+ };
247
+ }
248
+ throw error;
249
+ }
111
250
  });
112
251
  // Tool: Add note
113
252
  server.registerTool('add_note', {
@@ -117,9 +256,22 @@ server.registerTool('add_note', {
117
256
  taskId: z.string(),
118
257
  note: z.string(),
119
258
  }),
259
+ outputSchema: z.object({
260
+ taskId: z.string(),
261
+ success: z.boolean(),
262
+ }),
263
+ annotations: {
264
+ readOnlyHint: false,
265
+ destructiveHint: false,
266
+ idempotentHint: false,
267
+ openWorldHint: false,
268
+ },
120
269
  }, async ({ taskId, note }) => {
121
270
  await api('POST', `/api/agent/tasks/${taskId}/notes`, { note });
122
- return { content: [{ type: 'text', text: `Note added to ${taskId}` }] };
271
+ return {
272
+ content: [{ type: 'text', text: `Note added to ${taskId}` }],
273
+ structuredContent: { taskId, success: true },
274
+ };
123
275
  });
124
276
  // Tool: Complete task
125
277
  server.registerTool('complete_task', {
@@ -129,9 +281,48 @@ server.registerTool('complete_task', {
129
281
  taskId: z.string(),
130
282
  summary: z.string().describe('What was implemented'),
131
283
  }),
284
+ outputSchema: z.object({
285
+ id: z.string(),
286
+ status: z.string(),
287
+ }),
288
+ annotations: {
289
+ readOnlyHint: false,
290
+ destructiveHint: false,
291
+ idempotentHint: true,
292
+ openWorldHint: false,
293
+ },
132
294
  }, async ({ taskId, summary }) => {
133
295
  const result = await api('POST', `/api/agent/tasks/${taskId}/complete`, { summary });
134
- return { content: [{ type: 'text', text: `Completed ${result.id}` }] };
296
+ return {
297
+ content: [{ type: 'text', text: `Completed ${result.id}` }],
298
+ structuredContent: result,
299
+ };
300
+ });
301
+ // Tool: Abandon task
302
+ server.registerTool('abandon_task', {
303
+ title: 'Abandon Task',
304
+ description: 'Release lock and return task to planned status. Use when you cannot complete the task ' +
305
+ 'or need to stop working on it.',
306
+ inputSchema: z.object({
307
+ taskId: z.string(),
308
+ }),
309
+ outputSchema: z.object({
310
+ id: z.string(),
311
+ status: z.string(),
312
+ message: z.string(),
313
+ }),
314
+ annotations: {
315
+ readOnlyHint: false,
316
+ destructiveHint: false,
317
+ idempotentHint: true,
318
+ openWorldHint: false,
319
+ },
320
+ }, async ({ taskId }) => {
321
+ const result = await api('POST', `/api/agent/tasks/${taskId}/abandon`);
322
+ return {
323
+ content: [{ type: 'text', text: `Abandoned ${result.id}: ${result.message}` }],
324
+ structuredContent: result,
325
+ };
135
326
  });
136
327
  // Tool: List feedback
137
328
  server.registerTool('list_feedback', {
@@ -141,6 +332,15 @@ server.registerTool('list_feedback', {
141
332
  status: z.enum(['new', 'under_review', 'planned', 'in_progress', 'done', 'closed']).optional(),
142
333
  limit: z.number().optional(),
143
334
  }),
335
+ outputSchema: z.object({
336
+ feedback: z.array(FeedbackSummarySchema),
337
+ }),
338
+ annotations: {
339
+ readOnlyHint: true,
340
+ destructiveHint: false,
341
+ idempotentHint: true,
342
+ openWorldHint: false,
343
+ },
144
344
  }, async ({ status, limit }) => {
145
345
  const params = new URLSearchParams();
146
346
  if (status)
@@ -150,13 +350,19 @@ server.registerTool('list_feedback', {
150
350
  const query = params.toString();
151
351
  const data = await api('GET', `/api/agent/feedback${query ? `?${query}` : ''}`);
152
352
  if (!data.feedback.length) {
153
- return { content: [{ type: 'text', text: 'No feedback found' }] };
353
+ return {
354
+ content: [{ type: 'text', text: 'No feedback found' }],
355
+ structuredContent: { feedback: [] },
356
+ };
154
357
  }
155
358
  const lines = data.feedback.map((f) => {
156
359
  const link = f.linkedTaskId ? ` → ${f.linkedTaskId}` : '';
157
360
  return `• ${f.id}: ${f.title} [${f.type}] (${f.voterCount} votes)${link}`;
158
361
  });
159
- return { content: [{ type: 'text', text: `Feedback:\n${lines.join('\n')}` }] };
362
+ return {
363
+ content: [{ type: 'text', text: `Feedback:\n${lines.join('\n')}` }],
364
+ structuredContent: { feedback: data.feedback },
365
+ };
160
366
  });
161
367
  // Tool: Get feedback
162
368
  server.registerTool('get_feedback', {
@@ -165,6 +371,13 @@ server.registerTool('get_feedback', {
165
371
  inputSchema: z.object({
166
372
  feedbackId: z.string(),
167
373
  }),
374
+ outputSchema: FeedbackDetailSchema,
375
+ annotations: {
376
+ readOnlyHint: true,
377
+ destructiveHint: false,
378
+ idempotentHint: true,
379
+ openWorldHint: false,
380
+ },
168
381
  }, async ({ feedbackId }) => {
169
382
  const f = await api('GET', `/api/agent/feedback/${feedbackId}`);
170
383
  const parts = [
@@ -185,7 +398,10 @@ server.registerTool('get_feedback', {
185
398
  if (f.comments.length > 3)
186
399
  parts.push(`... and ${f.comments.length - 3} more`);
187
400
  }
188
- return { content: [{ type: 'text', text: parts.join('\n') }] };
401
+ return {
402
+ content: [{ type: 'text', text: parts.join('\n') }],
403
+ structuredContent: f,
404
+ };
189
405
  });
190
406
  // Start
191
407
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for Damper task management",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {
@@ -15,7 +15,9 @@
15
15
  },
16
16
  "main": "./dist/index.js",
17
17
  "types": "./dist/index.d.ts",
18
- "files": ["dist"],
18
+ "files": [
19
+ "dist"
20
+ ],
19
21
  "scripts": {
20
22
  "build": "tsc",
21
23
  "dev": "tsx src/index.ts",
@@ -33,6 +35,11 @@
33
35
  "engines": {
34
36
  "node": ">=18"
35
37
  },
36
- "keywords": ["mcp", "damper", "ai", "task-management"],
38
+ "keywords": [
39
+ "mcp",
40
+ "damper",
41
+ "ai",
42
+ "task-management"
43
+ ],
37
44
  "license": "MIT"
38
45
  }