@damper/mcp 0.1.2 → 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 +88 -15
  2. package/package.json +1 -1
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();
@@ -34,6 +40,7 @@ const server = new McpServer({
34
40
  const TaskSummarySchema = z.object({
35
41
  id: z.string(),
36
42
  title: z.string(),
43
+ type: z.string(),
37
44
  status: z.string(),
38
45
  priority: z.string(),
39
46
  feedbackCount: z.number(),
@@ -43,6 +50,7 @@ const TaskDetailSchema = z.object({
43
50
  id: z.string(),
44
51
  title: z.string(),
45
52
  description: z.string().optional(),
53
+ type: z.string(),
46
54
  implementationPlan: z.string().optional(),
47
55
  status: z.string(),
48
56
  voteScore: z.number(),
@@ -75,9 +83,11 @@ const FeedbackDetailSchema = z.object({
75
83
  // Tool: List tasks
76
84
  server.registerTool('list_tasks', {
77
85
  title: 'List Tasks',
78
- 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.',
79
88
  inputSchema: z.object({
80
89
  status: z.enum(['planned', 'in_progress', 'done', 'all']).optional(),
90
+ type: z.enum(['bug', 'feature', 'improvement', 'task']).optional().describe('Filter by task type'),
81
91
  limit: z.number().optional(),
82
92
  }),
83
93
  outputSchema: z.object({
@@ -90,23 +100,26 @@ server.registerTool('list_tasks', {
90
100
  idempotentHint: true,
91
101
  openWorldHint: false,
92
102
  },
93
- }, async ({ status, limit }) => {
103
+ }, async ({ status, type, limit }) => {
94
104
  const params = new URLSearchParams();
95
105
  if (status)
96
106
  params.set('status', status);
107
+ if (type)
108
+ params.set('type', type);
97
109
  if (limit)
98
110
  params.set('limit', String(limit));
99
111
  const query = params.toString();
100
112
  const data = await api('GET', `/api/agent/tasks${query ? `?${query}` : ''}`);
101
113
  if (!data.tasks.length) {
102
114
  return {
103
- content: [{ type: 'text', text: `No tasks in "${data.project.name}"` }],
115
+ content: [{ type: 'text', text: `No tasks in "${data.project.name}"${type ? ` (type: ${type})` : ''}` }],
104
116
  structuredContent: { project: data.project.name, tasks: [] },
105
117
  };
106
118
  }
107
119
  const lines = data.tasks.map((t) => {
108
120
  const p = t.priority === 'high' ? '🔴' : t.priority === 'medium' ? '🟡' : '⚪';
109
- 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 ? ' 📋' : ''}`;
110
123
  });
111
124
  return {
112
125
  content: [{ type: 'text', text: `Tasks in "${data.project.name}":\n${lines.join('\n')}` }],
@@ -129,9 +142,10 @@ server.registerTool('get_task', {
129
142
  },
130
143
  }, async ({ taskId }) => {
131
144
  const t = await api('GET', `/api/agent/tasks/${taskId}`);
145
+ const typeIcon = t.type === 'bug' ? '🐛' : t.type === 'feature' ? '✨' : t.type === 'improvement' ? '💡' : '📌';
132
146
  const parts = [
133
- `# ${t.title}`,
134
- `Status: ${t.status} | Score: ${t.voteScore}`,
147
+ `# ${typeIcon} ${t.title}`,
148
+ `Type: ${t.type} | Status: ${t.status} | Score: ${t.voteScore}`,
135
149
  ];
136
150
  if (t.description)
137
151
  parts.push(`\n## Description\n${t.description}`);
@@ -151,16 +165,18 @@ server.registerTool('get_task', {
151
165
  // Tool: Create task
152
166
  server.registerTool('create_task', {
153
167
  title: 'Create Task',
154
- 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.',
155
169
  inputSchema: z.object({
156
170
  title: z.string(),
157
171
  description: z.string().optional(),
172
+ type: z.enum(['bug', 'feature', 'improvement', 'task']).optional().describe('Task type (default: feature)'),
158
173
  status: z.enum(['planned', 'in_progress']).optional(),
159
174
  implementationPlan: z.string().optional(),
160
175
  }),
161
176
  outputSchema: z.object({
162
177
  id: z.string(),
163
178
  title: z.string(),
179
+ type: z.string(),
164
180
  status: z.string(),
165
181
  }),
166
182
  annotations: {
@@ -171,22 +187,27 @@ server.registerTool('create_task', {
171
187
  },
172
188
  }, async (args) => {
173
189
  const result = await api('POST', '/api/agent/tasks', args);
190
+ const typeIcon = result.type === 'bug' ? '🐛' : result.type === 'feature' ? '✨' : result.type === 'improvement' ? '💡' : '📌';
174
191
  return {
175
- content: [{ type: 'text', text: `Created: ${result.id} "${result.title}" [${result.status}]` }],
192
+ content: [{ type: 'text', text: `Created: ${result.id} ${typeIcon} "${result.title}" [${result.status}]` }],
176
193
  structuredContent: result,
177
194
  };
178
195
  });
179
196
  // Tool: Start task
180
197
  server.registerTool('start_task', {
181
198
  title: 'Start Task',
182
- 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).',
183
201
  inputSchema: z.object({
184
202
  taskId: z.string(),
203
+ force: z.boolean().optional().describe('Take over lock from another agent'),
185
204
  }),
186
205
  outputSchema: z.object({
187
206
  id: z.string(),
188
207
  status: z.string(),
189
208
  message: z.string(),
209
+ lockedBy: z.string().optional(),
210
+ lockedAt: z.string().optional(),
190
211
  }),
191
212
  annotations: {
192
213
  readOnlyHint: false,
@@ -194,12 +215,38 @@ server.registerTool('start_task', {
194
215
  idempotentHint: true,
195
216
  openWorldHint: false,
196
217
  },
197
- }, async ({ taskId }) => {
198
- const result = await api('POST', `/api/agent/tasks/${taskId}/start`);
199
- return {
200
- content: [{ type: 'text', text: `Started ${result.id}: ${result.message}` }],
201
- structuredContent: result,
202
- };
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
+ }
203
250
  });
204
251
  // Tool: Add note
205
252
  server.registerTool('add_note', {
@@ -251,6 +298,32 @@ server.registerTool('complete_task', {
251
298
  structuredContent: result,
252
299
  };
253
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
+ };
326
+ });
254
327
  // Tool: List feedback
255
328
  server.registerTool('list_feedback', {
256
329
  title: 'List Feedback',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for Damper task management",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {