@damper/mcp 0.1.2 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +56 -8
  2. package/dist/index.js +88 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -40,25 +40,73 @@ Damper → Settings → API Keys → Generate
40
40
  }
41
41
  ```
42
42
 
43
+ ### Multiple Projects
44
+
45
+ Each API key is tied to one project. For multiple projects, configure separate server entries:
46
+
47
+ **Claude Code** (`~/.claude.json`):
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "damper-frontend": {
52
+ "command": "damper",
53
+ "env": { "DAMPER_API_KEY": "dmp_frontend_xxx" }
54
+ },
55
+ "damper-backend": {
56
+ "command": "damper",
57
+ "env": { "DAMPER_API_KEY": "dmp_backend_xxx" }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ The AI will see tools from both servers and can distinguish between them:
64
+ - "list tasks from damper-frontend"
65
+ - "start the auth bug in damper-backend"
66
+
43
67
  ## Tools
44
68
 
45
69
  | Tool | Description |
46
70
  |------|-------------|
47
- | `list_tasks` | Get roadmap tasks |
48
- | `get_task` | Task details + feedback |
49
- | `create_task` | Create task (for TODO imports) |
50
- | `start_task` | Mark in-progress |
71
+ | `list_tasks` | Get roadmap tasks (filter by `status`, `type`) |
72
+ | `get_task` | Task details + linked feedback |
73
+ | `create_task` | Create task with type (bug, feature, improvement, task) |
74
+ | `start_task` | Lock and start task (use `force` to take over) |
51
75
  | `add_note` | Add progress note |
52
- | `complete_task` | Mark done |
76
+ | `complete_task` | Mark done, release lock |
77
+ | `abandon_task` | Release lock, return to planned |
53
78
  | `list_feedback` | Browse user feedback |
54
- | `get_feedback` | Feedback details |
79
+ | `get_feedback` | Feedback details + votes |
80
+
81
+ ### Task Types
82
+
83
+ Filter tasks by type to prioritize work:
84
+
85
+ ```
86
+ > List all bugs
87
+ > Work on the first feature
88
+ > Show me improvement tasks
89
+ ```
90
+
91
+ Types: `bug` 🐛, `feature` ✨, `improvement` 💡, `task` 📌
92
+
93
+ ### Task Locking
94
+
95
+ When you start a task, it's locked to prevent other agents from working on it simultaneously:
96
+
97
+ - Same agent starting again → success (idempotent)
98
+ - Different agent starting → fails with 409 (use `force: true` to take over)
99
+ - Complete or abandon → releases lock
55
100
 
56
- ## Usage
101
+ ## Usage Examples
57
102
 
58
103
  ```
59
104
  > What tasks are available?
105
+ > List bugs I can fix
60
106
  > Import my TODO.md into Damper
61
- > Work on the dark mode task
107
+ > Work on the dark mode feature
108
+ > I'm done with the auth task - tests pass
109
+ > Abandon the current task, I'm blocked
62
110
  ```
63
111
 
64
112
  ## Environment
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.4",
4
4
  "description": "MCP server for Damper task management",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {