@zoobbe/cli 1.2.1 → 1.2.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.
- package/README.md +42 -0
- package/bin/zoobbe-mcp +5 -0
- package/package.json +6 -3
- package/src/index.js +4 -0
- package/src/lib/client.js +23 -6
- package/src/mcp-server.js +800 -0
package/README.md
CHANGED
|
@@ -365,6 +365,48 @@ zoobbe t start <cardId> # Same as: zoobbe timer start <cardId>
|
|
|
365
365
|
zoobbe n ls --unread # Same as: zoobbe notification list --unread
|
|
366
366
|
```
|
|
367
367
|
|
|
368
|
+
## MCP Server (AI Integration)
|
|
369
|
+
|
|
370
|
+
The CLI includes a built-in [MCP](https://modelcontextprotocol.io) server that lets AI agents like Claude interact with your Zoobbe workspace directly.
|
|
371
|
+
|
|
372
|
+
### Setup
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Install the CLI (includes MCP server)
|
|
376
|
+
npm install -g @zoobbe/cli
|
|
377
|
+
|
|
378
|
+
# Make sure you're logged in
|
|
379
|
+
zoobbe auth login
|
|
380
|
+
|
|
381
|
+
# Connect to Claude Code
|
|
382
|
+
claude mcp add zoobbe -- zoobbe-mcp
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### What Claude can do
|
|
386
|
+
|
|
387
|
+
Once connected, Claude can read and manage your workspace through natural language:
|
|
388
|
+
|
|
389
|
+
- "What cards are assigned to me?"
|
|
390
|
+
- "Show overdue tasks on the Sprint board"
|
|
391
|
+
- "Create a high-priority card for the login bug"
|
|
392
|
+
- "Move card abc123 to Done"
|
|
393
|
+
- "What happened on the board this week?"
|
|
394
|
+
|
|
395
|
+
### Source Attribution
|
|
396
|
+
|
|
397
|
+
All changes made through the MCP server are automatically tagged in activity logs as **(via AI Agent)**, and CLI commands are tagged as **(via CLI)**. This lets your team distinguish between manual and automated actions when reviewing card or board history.
|
|
398
|
+
|
|
399
|
+
### Available tools (28)
|
|
400
|
+
|
|
401
|
+
| Category | Tools |
|
|
402
|
+
|----------|-------|
|
|
403
|
+
| Boards | `list_boards`, `get_board`, `list_lists`, `board_members`, `board_activities`, `board_labels`, `board_analytics` |
|
|
404
|
+
| Cards | `list_cards`, `get_card`, `card_comments`, `card_checklists`, `card_activities` |
|
|
405
|
+
| Actions | `create_card`, `update_card`, `move_card`, `assign_card`, `add_comment`, `mark_card_done`, `mark_card_undone`, `set_due_date`, `set_priority`, `archive_card` |
|
|
406
|
+
| General | `my_status`, `search`, `list_notifications`, `mark_notifications_read`, `create_board`, `create_list` |
|
|
407
|
+
|
|
408
|
+
Zero extra dependencies — the MCP server implements the protocol natively over stdio.
|
|
409
|
+
|
|
368
410
|
## License
|
|
369
411
|
|
|
370
412
|
MIT
|
package/bin/zoobbe-mcp
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoobbe/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Zoobbe CLI - Manage boards, cards, and projects from the terminal",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"zoobbe": "bin/zoobbe"
|
|
7
|
+
"zoobbe": "bin/zoobbe",
|
|
8
|
+
"zoobbe-mcp": "bin/zoobbe-mcp"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "node bin/zoobbe",
|
|
@@ -19,7 +20,9 @@
|
|
|
19
20
|
"task-management",
|
|
20
21
|
"boards",
|
|
21
22
|
"cards",
|
|
22
|
-
"ai-agent"
|
|
23
|
+
"ai-agent",
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol"
|
|
23
26
|
],
|
|
24
27
|
"license": "MIT",
|
|
25
28
|
"author": {
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const pkg = require('../package.json');
|
|
4
|
+
const client = require('./lib/client');
|
|
5
|
+
|
|
6
|
+
// Mark all requests from CLI as CLI actions
|
|
7
|
+
client.setSource('cli');
|
|
4
8
|
|
|
5
9
|
const program = new Command();
|
|
6
10
|
|
package/src/lib/client.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
const config = require('./config');
|
|
2
2
|
|
|
3
3
|
class ZoobbeClient {
|
|
4
|
+
constructor() {
|
|
5
|
+
this._source = null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
setSource(source) {
|
|
9
|
+
this._source = source;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
get baseUrl() {
|
|
5
13
|
return config.get('apiUrl');
|
|
6
14
|
}
|
|
@@ -10,11 +18,15 @@ class ZoobbeClient {
|
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
get headers() {
|
|
13
|
-
|
|
21
|
+
const h = {
|
|
14
22
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
15
23
|
'Content-Type': 'application/json',
|
|
16
24
|
'User-Agent': 'Zoobbe-CLI/1.0',
|
|
17
25
|
};
|
|
26
|
+
if (this._source) {
|
|
27
|
+
h['X-Zoobbe-Source'] = this._source;
|
|
28
|
+
}
|
|
29
|
+
return h;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
ensureAuth() {
|
|
@@ -96,13 +108,18 @@ class ZoobbeClient {
|
|
|
96
108
|
|
|
97
109
|
const body = Buffer.concat([Buffer.from(header), fileBuffer, Buffer.from(footer)]);
|
|
98
110
|
|
|
111
|
+
const uploadHeaders = {
|
|
112
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
113
|
+
'User-Agent': 'Zoobbe-CLI/1.0',
|
|
114
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
115
|
+
};
|
|
116
|
+
if (this._source) {
|
|
117
|
+
uploadHeaders['X-Zoobbe-Source'] = this._source;
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
const response = await fetch(url, {
|
|
100
121
|
method: 'POST',
|
|
101
|
-
headers:
|
|
102
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
103
|
-
'User-Agent': 'Zoobbe-CLI/1.0',
|
|
104
|
-
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
105
|
-
},
|
|
122
|
+
headers: uploadHeaders,
|
|
106
123
|
body,
|
|
107
124
|
});
|
|
108
125
|
|
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zoobbe MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes Zoobbe boards, cards, and project management as MCP tools
|
|
7
|
+
* so AI agents (Claude, etc.) can interact with your workspace.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* claude mcp add zoobbe -- zoobbe-mcp
|
|
11
|
+
*
|
|
12
|
+
* Protocol: JSON-RPC 2.0 over stdio (MCP standard)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
const client = require('./lib/client');
|
|
17
|
+
const config = require('./lib/config');
|
|
18
|
+
|
|
19
|
+
// Mark all requests from MCP server as AI agent actions
|
|
20
|
+
client.setSource('mcp');
|
|
21
|
+
|
|
22
|
+
// ── Tool Definitions ──
|
|
23
|
+
|
|
24
|
+
const TOOLS = [
|
|
25
|
+
// Read — Boards
|
|
26
|
+
{
|
|
27
|
+
name: 'list_boards',
|
|
28
|
+
description: 'List all boards in the active Zoobbe workspace. Returns board names, IDs, visibility, member/list counts, and archive status.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
include_archived: { type: 'boolean', description: 'Include archived boards (default false)' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'get_board',
|
|
38
|
+
description: 'Get details of a specific board including title, visibility, member count, list count, and creation date.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
43
|
+
},
|
|
44
|
+
required: ['board_id'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'list_lists',
|
|
49
|
+
description: 'List all columns (action lists) on a board. Returns list titles, IDs, card counts, and status.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
54
|
+
},
|
|
55
|
+
required: ['board_id'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'board_members',
|
|
60
|
+
description: 'List all members of a board with their names, emails, and roles.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
65
|
+
},
|
|
66
|
+
required: ['board_id'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'board_activities',
|
|
71
|
+
description: 'Show recent activity log for a board (who did what and when).',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
76
|
+
limit: { type: 'number', description: 'Max activities to return (default 20)' },
|
|
77
|
+
},
|
|
78
|
+
required: ['board_id'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'board_labels',
|
|
83
|
+
description: 'List all labels available on a board.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
88
|
+
},
|
|
89
|
+
required: ['board_id'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Read — Cards
|
|
94
|
+
{
|
|
95
|
+
name: 'list_cards',
|
|
96
|
+
description: 'List cards on a board. Can filter by list, assignee, due date. Returns titles, IDs, list names, due dates, members, and priority.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
101
|
+
due: { type: 'string', description: 'Filter: "today", "week", or "overdue"' },
|
|
102
|
+
},
|
|
103
|
+
required: ['board_id'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'get_card',
|
|
108
|
+
description: 'Get full details of a card: title, description, board, list, members, due date, priority, completion status, labels.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
113
|
+
},
|
|
114
|
+
required: ['card_id'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'card_comments',
|
|
119
|
+
description: 'List comments on a card.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
124
|
+
},
|
|
125
|
+
required: ['card_id'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'card_checklists',
|
|
130
|
+
description: 'List checklists and their items on a card, with completion status.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
135
|
+
},
|
|
136
|
+
required: ['card_id'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'card_activities',
|
|
141
|
+
description: 'Show recent activity log for a card.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
146
|
+
limit: { type: 'number', description: 'Max activities (default 20)' },
|
|
147
|
+
},
|
|
148
|
+
required: ['card_id'],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Read — General
|
|
153
|
+
{
|
|
154
|
+
name: 'my_status',
|
|
155
|
+
description: 'Show all cards assigned to the current user across all boards. Great for "what am I working on?" questions.',
|
|
156
|
+
inputSchema: { type: 'object', properties: {} },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'search',
|
|
160
|
+
description: 'Search across boards, cards, and pages. Returns matching results with titles and IDs.',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
query: { type: 'string', description: 'Search query' },
|
|
165
|
+
},
|
|
166
|
+
required: ['query'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'list_notifications',
|
|
171
|
+
description: 'List recent notifications. Can filter to unread only.',
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
unread_only: { type: 'boolean', description: 'Only unread (default true)' },
|
|
176
|
+
limit: { type: 'number', description: 'Max notifications (default 20)' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'board_analytics',
|
|
182
|
+
description: 'Get analytics overview for a board: total cards, completed, in progress, overdue, completion rate.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
187
|
+
},
|
|
188
|
+
required: ['board_id'],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// Write — Cards
|
|
193
|
+
{
|
|
194
|
+
name: 'create_card',
|
|
195
|
+
description: 'Create a new card on a board. Optionally specify list, description, due date, and priority.',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
title: { type: 'string', description: 'Card title' },
|
|
200
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
201
|
+
list_id: { type: 'string', description: 'List ID (uses first list if omitted)' },
|
|
202
|
+
description: { type: 'string', description: 'Card description' },
|
|
203
|
+
due_date: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
|
|
204
|
+
priority: { type: 'string', description: 'Priority: low, medium, high, critical' },
|
|
205
|
+
},
|
|
206
|
+
required: ['title', 'board_id'],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'update_card',
|
|
211
|
+
description: 'Update a card\'s title or description.',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
216
|
+
title: { type: 'string', description: 'New title' },
|
|
217
|
+
description: { type: 'string', description: 'New description' },
|
|
218
|
+
},
|
|
219
|
+
required: ['card_id'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'move_card',
|
|
224
|
+
description: 'Move a card to a different list (column) on its board.',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
229
|
+
list_name: { type: 'string', description: 'Target list name or ID' },
|
|
230
|
+
board_id: { type: 'string', description: 'Board short ID (auto-detected if omitted)' },
|
|
231
|
+
},
|
|
232
|
+
required: ['card_id', 'list_name'],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'assign_card',
|
|
237
|
+
description: 'Assign a user to a card.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
242
|
+
user_id: { type: 'string', description: 'User ID to assign' },
|
|
243
|
+
},
|
|
244
|
+
required: ['card_id', 'user_id'],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'add_comment',
|
|
249
|
+
description: 'Add a comment to a card.',
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
254
|
+
comment: { type: 'string', description: 'Comment text' },
|
|
255
|
+
},
|
|
256
|
+
required: ['card_id', 'comment'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'mark_card_done',
|
|
261
|
+
description: 'Mark a card as complete.',
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
266
|
+
},
|
|
267
|
+
required: ['card_id'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'mark_card_undone',
|
|
272
|
+
description: 'Mark a card as incomplete.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
277
|
+
},
|
|
278
|
+
required: ['card_id'],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'set_due_date',
|
|
283
|
+
description: 'Set or remove a due date on a card.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
288
|
+
due_date: { type: 'string', description: 'Due date (YYYY-MM-DD), or empty to remove' },
|
|
289
|
+
},
|
|
290
|
+
required: ['card_id'],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'set_priority',
|
|
295
|
+
description: 'Set card priority level.',
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
300
|
+
priority: { type: 'string', description: 'Priority: low, medium, high, critical' },
|
|
301
|
+
},
|
|
302
|
+
required: ['card_id', 'priority'],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'archive_card',
|
|
307
|
+
description: 'Archive a card.',
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
card_id: { type: 'string', description: 'Card short ID' },
|
|
312
|
+
},
|
|
313
|
+
required: ['card_id'],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// Write — Board/List
|
|
318
|
+
{
|
|
319
|
+
name: 'create_board',
|
|
320
|
+
description: 'Create a new board in the active workspace.',
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
properties: {
|
|
324
|
+
name: { type: 'string', description: 'Board name' },
|
|
325
|
+
visibility: { type: 'string', description: 'Private, Public, or Workspace (default Private)' },
|
|
326
|
+
},
|
|
327
|
+
required: ['name'],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'create_list',
|
|
332
|
+
description: 'Create a new list (column) on a board.',
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
title: { type: 'string', description: 'List title' },
|
|
337
|
+
board_id: { type: 'string', description: 'Board short ID' },
|
|
338
|
+
},
|
|
339
|
+
required: ['title', 'board_id'],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'mark_notifications_read',
|
|
344
|
+
description: 'Mark all notifications as read.',
|
|
345
|
+
inputSchema: { type: 'object', properties: {} },
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
// ── Tool Handlers ──
|
|
350
|
+
|
|
351
|
+
async function executeTool(name, args = {}) {
|
|
352
|
+
const workspaceId = config.get('activeWorkspace');
|
|
353
|
+
|
|
354
|
+
switch (name) {
|
|
355
|
+
// ── Read: Boards ──
|
|
356
|
+
case 'list_boards': {
|
|
357
|
+
const data = await client.get(`/boards?workspaceId=${encodeURIComponent(workspaceId)}`);
|
|
358
|
+
let boards = data.data || data;
|
|
359
|
+
if (!args.include_archived) {
|
|
360
|
+
boards = boards.filter(b => !b.isArchived);
|
|
361
|
+
}
|
|
362
|
+
return boards.map(b => ({
|
|
363
|
+
name: b.name || b.title,
|
|
364
|
+
id: b.shortId,
|
|
365
|
+
visibility: b.visibility || 'Private',
|
|
366
|
+
lists: b.actionLists?.length || 0,
|
|
367
|
+
members: b.members?.length || 0,
|
|
368
|
+
archived: !!b.isArchived,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
case 'get_board': {
|
|
373
|
+
const data = await client.get(`/boards/${args.board_id}`);
|
|
374
|
+
const b = data.board || data.data || data;
|
|
375
|
+
return {
|
|
376
|
+
name: b.title || b.name,
|
|
377
|
+
id: b.shortId,
|
|
378
|
+
visibility: b.visibility || 'Private',
|
|
379
|
+
members: b.members?.length || 0,
|
|
380
|
+
lists: b.actionLists?.length || 0,
|
|
381
|
+
created: b.createdAt,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
case 'list_lists': {
|
|
386
|
+
const data = await client.get(`/${args.board_id}/actionLists/`);
|
|
387
|
+
let lists = data.lists || data.data || data;
|
|
388
|
+
lists = Array.isArray(lists) ? lists : Object.values(lists);
|
|
389
|
+
return lists.filter(l => !l.archived).map(l => ({
|
|
390
|
+
title: l.title || 'Untitled',
|
|
391
|
+
id: l._id,
|
|
392
|
+
cards: l.cards?.length || 0,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
case 'board_members': {
|
|
397
|
+
const data = await client.get(`/boards/${args.board_id}/members`);
|
|
398
|
+
const members = data.members || data.data || data;
|
|
399
|
+
return (Array.isArray(members) ? members : []).map(m => {
|
|
400
|
+
const u = m.user || m;
|
|
401
|
+
return { name: u.userName || u.name, email: u.email, role: m.role || 'member', id: u._id };
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case 'board_activities': {
|
|
406
|
+
const data = await client.get(`/boards/${args.board_id}/activities`);
|
|
407
|
+
const acts = data.activities || data.data || data;
|
|
408
|
+
return (Array.isArray(acts) ? acts : []).slice(0, args.limit || 20).map(a => ({
|
|
409
|
+
action: a.action || a.type,
|
|
410
|
+
user: a.user?.userName || a.user?.name,
|
|
411
|
+
target: a.target || a.card?.title || '',
|
|
412
|
+
time: a.createdAt,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'board_labels': {
|
|
417
|
+
const data = await client.get(`/boards/${args.board_id}/labels`);
|
|
418
|
+
const labels = data.labels || data.data || data;
|
|
419
|
+
return (Array.isArray(labels) ? labels : []).map(l => ({
|
|
420
|
+
text: l.text || '', color: l.color || '', id: l._id,
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── Read: Cards ──
|
|
425
|
+
case 'list_cards': {
|
|
426
|
+
const listsData = await client.get(`/boards/${args.board_id}/lists`);
|
|
427
|
+
const lists = listsData.lists || listsData.data || listsData;
|
|
428
|
+
const listArr = Array.isArray(lists) ? lists : Object.values(lists);
|
|
429
|
+
|
|
430
|
+
const cardRefs = [];
|
|
431
|
+
for (const list of listArr) {
|
|
432
|
+
if (list.cards && Array.isArray(list.cards)) {
|
|
433
|
+
for (const c of list.cards) {
|
|
434
|
+
cardRefs.push({ id: c.shortId || c._id, listName: list.title || '' });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const results = await Promise.allSettled(
|
|
440
|
+
cardRefs.map(ref => client.get(`/cards/${ref.id}`).then(d => ({ ...d, _listName: ref.listName })))
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
let cards = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
|
444
|
+
|
|
445
|
+
if (args.due) {
|
|
446
|
+
const now = new Date();
|
|
447
|
+
cards = cards.filter(c => {
|
|
448
|
+
if (!c.dueDate) return false;
|
|
449
|
+
const due = new Date(c.dueDate?.date || c.dueDate);
|
|
450
|
+
if (args.due === 'today') return due.toDateString() === now.toDateString();
|
|
451
|
+
if (args.due === 'week') return due <= new Date(now.getTime() + 7 * 86400000);
|
|
452
|
+
if (args.due === 'overdue') return due < now;
|
|
453
|
+
return true;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return cards.map(c => ({
|
|
458
|
+
title: c.title || c.name,
|
|
459
|
+
id: c.shortId || c._id,
|
|
460
|
+
list: c._listName || c.actionListTitle || '',
|
|
461
|
+
due: c.dueDate?.date || c.dueDate || null,
|
|
462
|
+
members: c.members?.length || 0,
|
|
463
|
+
priority: c.priority || null,
|
|
464
|
+
complete: !!c.completed,
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case 'get_card': {
|
|
469
|
+
const data = await client.get(`/cards/${args.card_id}`);
|
|
470
|
+
const c = data.card || data.data || data;
|
|
471
|
+
return {
|
|
472
|
+
title: c.title || c.name,
|
|
473
|
+
id: c.shortId || c._id,
|
|
474
|
+
board: c.boardTitle || c.board,
|
|
475
|
+
list: c.actionListTitle || c.actionList?.name,
|
|
476
|
+
description: c.description || '',
|
|
477
|
+
members: c.members?.length || 0,
|
|
478
|
+
due: c.dueDate?.date || c.dueDate || null,
|
|
479
|
+
priority: c.priority || null,
|
|
480
|
+
complete: !!c.completed,
|
|
481
|
+
labels: c.labels || [],
|
|
482
|
+
created: c.createdAt,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
case 'card_comments': {
|
|
487
|
+
const data = await client.get(`/cards/${args.card_id}/comments`);
|
|
488
|
+
const comments = data.comments || data.data || data;
|
|
489
|
+
return (Array.isArray(comments) ? comments : []).map(c => ({
|
|
490
|
+
user: c.member?.userName || c.user?.userName,
|
|
491
|
+
comment: c.comment,
|
|
492
|
+
time: c.createdAt,
|
|
493
|
+
id: c._id,
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
case 'card_checklists': {
|
|
498
|
+
const data = await client.get(`/cards/${args.card_id}/checklists`);
|
|
499
|
+
const cls = data.checklists || data.data || data;
|
|
500
|
+
return (Array.isArray(cls) ? cls : []).map(cl => ({
|
|
501
|
+
title: cl.title,
|
|
502
|
+
id: cl._id,
|
|
503
|
+
items: (cl.items || []).map(i => ({
|
|
504
|
+
title: i.title, checked: !!i.checked, id: i._id,
|
|
505
|
+
})),
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
case 'card_activities': {
|
|
510
|
+
const data = await client.get(`/cards/${args.card_id}/activities`);
|
|
511
|
+
const acts = data.activities || data.data || data;
|
|
512
|
+
return (Array.isArray(acts) ? acts : []).slice(0, args.limit || 20).map(a => ({
|
|
513
|
+
action: a.action || a.type,
|
|
514
|
+
user: a.user?.userName || a.user?.name,
|
|
515
|
+
detail: a.detail || a.description || '',
|
|
516
|
+
time: a.createdAt,
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── Read: General ──
|
|
521
|
+
case 'my_status': {
|
|
522
|
+
const userId = config.get('userId');
|
|
523
|
+
const data = await client.get(`/${userId}/cards`);
|
|
524
|
+
const cards = data.data || data;
|
|
525
|
+
return (Array.isArray(cards) ? cards : []).map(c => ({
|
|
526
|
+
title: c.title || c.name,
|
|
527
|
+
id: c.shortId || c._id,
|
|
528
|
+
board: c.boardTitle || '',
|
|
529
|
+
list: c.actionListTitle || '',
|
|
530
|
+
due: c.dueDate?.date || c.dueDate || null,
|
|
531
|
+
priority: c.priority || null,
|
|
532
|
+
complete: !!c.completed,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
case 'search': {
|
|
537
|
+
const data = await client.get(`/search?q=${encodeURIComponent(args.query)}&workspaceId=${encodeURIComponent(workspaceId)}`);
|
|
538
|
+
return data.data || data;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case 'list_notifications': {
|
|
542
|
+
const limit = args.limit || 20;
|
|
543
|
+
let url = `/notifications?limit=${limit}`;
|
|
544
|
+
if (args.unread_only !== false) url += '&unread=true';
|
|
545
|
+
const data = await client.get(url);
|
|
546
|
+
const notifs = data.notifications || data.data || data;
|
|
547
|
+
return (Array.isArray(notifs) ? notifs : []).map(n => ({
|
|
548
|
+
type: n.type || n.action,
|
|
549
|
+
message: n.message || n.text || '',
|
|
550
|
+
read: !!n.read,
|
|
551
|
+
time: n.createdAt,
|
|
552
|
+
id: n._id,
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
case 'board_analytics': {
|
|
557
|
+
const data = await client.get(`/analytics/board/${args.board_id}`);
|
|
558
|
+
return data.analytics || data.data || data;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── Write: Cards ──
|
|
562
|
+
case 'create_card': {
|
|
563
|
+
let listId = args.list_id;
|
|
564
|
+
if (!listId) {
|
|
565
|
+
const listsData = await client.get(`/boards/${args.board_id}/lists`);
|
|
566
|
+
const lists = listsData.lists || listsData.data || listsData;
|
|
567
|
+
const listArr = Array.isArray(lists) ? lists : Object.values(lists);
|
|
568
|
+
if (listArr.length === 0) throw new Error('Board has no lists');
|
|
569
|
+
listId = listArr[0]._id;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const body = { title: args.title, actionListId: listId };
|
|
573
|
+
if (args.description) body.description = args.description;
|
|
574
|
+
|
|
575
|
+
const data = await client.post('/cards', body);
|
|
576
|
+
const c = data.card || data.data || data;
|
|
577
|
+
const cardId = c.shortId || c._id;
|
|
578
|
+
|
|
579
|
+
if (args.due_date) {
|
|
580
|
+
await client.post(`/cards/${cardId}/duedate`, { dueDate: args.due_date });
|
|
581
|
+
}
|
|
582
|
+
if (args.priority) {
|
|
583
|
+
await client.post(`/cards/${cardId}/priority`, { priority: args.priority });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return { created: true, title: args.title, id: cardId, changed_by: 'AI Agent (via MCP)' };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
case 'update_card': {
|
|
590
|
+
const body = {};
|
|
591
|
+
if (args.title) body.title = args.title;
|
|
592
|
+
if (args.description) body.description = args.description;
|
|
593
|
+
if (Object.keys(body).length === 0) throw new Error('Provide title or description to update');
|
|
594
|
+
await client.post(`/cards/update/${args.card_id}`, body);
|
|
595
|
+
return { updated: true, card_id: args.card_id, changed_by: 'AI Agent (via MCP)' };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
case 'move_card': {
|
|
599
|
+
const cardData = await client.get(`/cards/${args.card_id}`);
|
|
600
|
+
const c = cardData.card || cardData.data || cardData;
|
|
601
|
+
const boardId = args.board_id || c.boardShortId || c.board;
|
|
602
|
+
|
|
603
|
+
const listsData = await client.get(`/boards/${boardId}/lists`);
|
|
604
|
+
const lists = listsData.lists || listsData.data || listsData;
|
|
605
|
+
const listArr = Array.isArray(lists) ? lists : Object.values(lists);
|
|
606
|
+
|
|
607
|
+
const sourceListId = c.actionList?._id || c.actionListId;
|
|
608
|
+
const sourceList = listArr.find(l => l._id === sourceListId || l._id?.toString() === sourceListId?.toString());
|
|
609
|
+
const targetList = listArr.find(l =>
|
|
610
|
+
l._id === args.list_name ||
|
|
611
|
+
(l.title || '').toLowerCase() === args.list_name.toLowerCase()
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
if (!targetList) throw new Error(`List "${args.list_name}" not found. Available: ${listArr.map(l => l.title).join(', ')}`);
|
|
615
|
+
if (!sourceList) throw new Error('Could not determine current list');
|
|
616
|
+
|
|
617
|
+
await client.put(`/boards/${boardId}/cards/order`, {
|
|
618
|
+
sourceListId: sourceList._id,
|
|
619
|
+
cardId: c._id,
|
|
620
|
+
targetListId: targetList._id,
|
|
621
|
+
targetPosition: 0,
|
|
622
|
+
});
|
|
623
|
+
return { moved: true, card_id: args.card_id, to: targetList.title, changed_by: 'AI Agent (via MCP)' };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
case 'assign_card': {
|
|
627
|
+
await client.post(`/cards/${args.card_id}/addMember`, { memberId: args.user_id });
|
|
628
|
+
return { assigned: true, card_id: args.card_id, user_id: args.user_id, changed_by: 'AI Agent (via MCP)' };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
case 'add_comment': {
|
|
632
|
+
const userId = config.get('userId');
|
|
633
|
+
await client.post(`/cards/${args.card_id}/comments`, {
|
|
634
|
+
comment: args.comment, member: userId, mentionedIds: [],
|
|
635
|
+
});
|
|
636
|
+
return { commented: true, card_id: args.card_id, changed_by: 'AI Agent (via MCP)' };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
case 'mark_card_done': {
|
|
640
|
+
await client.post(`/cards/update/${args.card_id}`, { isComplete: true });
|
|
641
|
+
return { done: true, card_id: args.card_id, changed_by: 'AI Agent (via MCP)' };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
case 'mark_card_undone': {
|
|
645
|
+
await client.post(`/cards/update/${args.card_id}`, { isComplete: false });
|
|
646
|
+
return { undone: true, card_id: args.card_id, changed_by: 'AI Agent (via MCP)' };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
case 'set_due_date': {
|
|
650
|
+
if (!args.due_date) {
|
|
651
|
+
await client.delete(`/cards/${args.card_id}/duedate`);
|
|
652
|
+
return { removed: true, card_id: args.card_id, changed_by: 'AI Agent (via MCP)' };
|
|
653
|
+
}
|
|
654
|
+
await client.post(`/cards/${args.card_id}/duedate`, { dueDate: args.due_date });
|
|
655
|
+
return { set: true, card_id: args.card_id, due_date: args.due_date, changed_by: 'AI Agent (via MCP)' };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
case 'set_priority': {
|
|
659
|
+
await client.post(`/cards/${args.card_id}/priority`, { priority: args.priority });
|
|
660
|
+
return { set: true, card_id: args.card_id, priority: args.priority, changed_by: 'AI Agent (via MCP)' };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
case 'archive_card': {
|
|
664
|
+
await client.post(`/cards/archive/${args.card_id}`);
|
|
665
|
+
return { archived: true, card_id: args.card_id, changed_by: 'AI Agent (via MCP)' };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── Write: Board/List ──
|
|
669
|
+
case 'create_board': {
|
|
670
|
+
const wsData = await client.get('/workspaces/me');
|
|
671
|
+
const workspaces = wsData.data || wsData;
|
|
672
|
+
const ws = workspaces.find(w => w.shortId === workspaceId);
|
|
673
|
+
if (!ws) throw new Error('Active workspace not found');
|
|
674
|
+
|
|
675
|
+
const data = await client.post('/boards', {
|
|
676
|
+
title: args.name,
|
|
677
|
+
workspaceId: ws._id,
|
|
678
|
+
visibility: args.visibility || 'Private',
|
|
679
|
+
});
|
|
680
|
+
const b = data.board || data.data || data;
|
|
681
|
+
return { created: true, name: args.name, id: b.shortId, changed_by: 'AI Agent (via MCP)' };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
case 'create_list': {
|
|
685
|
+
const data = await client.post('/actionLists', { title: args.title, boardId: args.board_id });
|
|
686
|
+
const l = data.actionList || data.data || data;
|
|
687
|
+
return { created: true, title: args.title, id: l._id, changed_by: 'AI Agent (via MCP)' };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
case 'mark_notifications_read': {
|
|
691
|
+
await client.put('/notifications/mark-all-read');
|
|
692
|
+
return { done: true };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
default:
|
|
696
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ── MCP Protocol Handler (JSON-RPC 2.0 over stdio) ──
|
|
701
|
+
|
|
702
|
+
const SERVER_INFO = {
|
|
703
|
+
name: 'zoobbe',
|
|
704
|
+
version: '1.2.2',
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
function makeResponse(id, result) {
|
|
708
|
+
return JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n';
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function makeError(id, code, message) {
|
|
712
|
+
return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function handleMessage(msg) {
|
|
716
|
+
const { id, method, params } = msg;
|
|
717
|
+
|
|
718
|
+
switch (method) {
|
|
719
|
+
case 'initialize':
|
|
720
|
+
return makeResponse(id, {
|
|
721
|
+
protocolVersion: '2024-11-05',
|
|
722
|
+
capabilities: { tools: {} },
|
|
723
|
+
serverInfo: SERVER_INFO,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
case 'notifications/initialized':
|
|
727
|
+
return null; // no response for notifications
|
|
728
|
+
|
|
729
|
+
case 'tools/list':
|
|
730
|
+
return makeResponse(id, { tools: TOOLS });
|
|
731
|
+
|
|
732
|
+
case 'tools/call': {
|
|
733
|
+
const toolName = params?.name;
|
|
734
|
+
const toolArgs = params?.arguments || {};
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
const result = await executeTool(toolName, toolArgs);
|
|
738
|
+
return makeResponse(id, {
|
|
739
|
+
content: [{
|
|
740
|
+
type: 'text',
|
|
741
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
742
|
+
}],
|
|
743
|
+
});
|
|
744
|
+
} catch (err) {
|
|
745
|
+
return makeResponse(id, {
|
|
746
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
747
|
+
isError: true,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
case 'ping':
|
|
753
|
+
return makeResponse(id, {});
|
|
754
|
+
|
|
755
|
+
default:
|
|
756
|
+
if (id) {
|
|
757
|
+
return makeError(id, -32601, `Method not found: ${method}`);
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ── Main: stdio loop ──
|
|
764
|
+
|
|
765
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
766
|
+
|
|
767
|
+
let buffer = '';
|
|
768
|
+
|
|
769
|
+
process.stdin.on('data', (chunk) => {
|
|
770
|
+
buffer += chunk.toString();
|
|
771
|
+
|
|
772
|
+
let newlineIdx;
|
|
773
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
774
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
775
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
776
|
+
|
|
777
|
+
if (!line) continue;
|
|
778
|
+
|
|
779
|
+
let msg;
|
|
780
|
+
try {
|
|
781
|
+
msg = JSON.parse(line);
|
|
782
|
+
} catch {
|
|
783
|
+
process.stderr.write(`[zoobbe-mcp] Invalid JSON: ${line}\n`);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
handleMessage(msg).then((response) => {
|
|
788
|
+
if (response) {
|
|
789
|
+
process.stdout.write(response);
|
|
790
|
+
}
|
|
791
|
+
}).catch((err) => {
|
|
792
|
+
process.stderr.write(`[zoobbe-mcp] Error: ${err.message}\n`);
|
|
793
|
+
if (msg.id) {
|
|
794
|
+
process.stdout.write(makeError(msg.id, -32603, err.message));
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
process.stderr.write('[zoobbe-mcp] Server started\n');
|