@delorenj/mcp-server-trello 1.7.0 → 1.8.0

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 (4) hide show
  1. package/README.md +185 -138
  2. package/build/index.js +8594 -1011
  3. package/package.json +19 -6
  4. package/src/index.ts +1878 -0
package/src/index.ts ADDED
@@ -0,0 +1,1878 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
5
+ // The SDK types its schemas against zod/v4; importing bare 'zod' yields the v3 API,
6
+ // which makes registerTool's inference explode (TS2589) and OOMs tsc.
7
+ import { z } from 'zod/v4';
8
+ import { TrelloClient } from './trello-client.js';
9
+ import { TrelloHealthEndpoints, HealthEndpointSchemas } from './health/health-endpoints.js';
10
+ import { formatCardListResponse } from './card-list-preview.js';
11
+
12
+ class TrelloServer {
13
+ private server: McpServer;
14
+ private trelloClient: TrelloClient;
15
+ private healthEndpoints: TrelloHealthEndpoints;
16
+
17
+ constructor() {
18
+ const apiKey = process.env.TRELLO_API_KEY;
19
+ const token = process.env.TRELLO_TOKEN;
20
+ const defaultBoardId = process.env.TRELLO_BOARD_ID;
21
+ const allowedWorkspacesEnv = process.env.TRELLO_ALLOWED_WORKSPACES;
22
+
23
+ if (!apiKey || !token) {
24
+ throw new Error('TRELLO_API_KEY and TRELLO_TOKEN environment variables are required');
25
+ }
26
+
27
+ // Parse allowed workspaces from comma-separated string
28
+ const allowedWorkspaceIds = allowedWorkspacesEnv
29
+ ? allowedWorkspacesEnv.split(',').map(id => id.trim()).filter(id => id.length > 0)
30
+ : undefined;
31
+
32
+ this.trelloClient = new TrelloClient({
33
+ apiKey,
34
+ token,
35
+ defaultBoardId,
36
+ boardId: defaultBoardId,
37
+ allowedWorkspaceIds,
38
+ });
39
+
40
+ this.healthEndpoints = new TrelloHealthEndpoints(this.trelloClient);
41
+
42
+ this.server = new McpServer({
43
+ name: 'trello-server',
44
+ version: '1.8.0',
45
+ });
46
+
47
+ this.setupTools();
48
+ this.setupHealthEndpoints();
49
+
50
+ // Error handling
51
+ process.on('SIGINT', async () => {
52
+ await this.server.close();
53
+ process.exit(0);
54
+ });
55
+ }
56
+
57
+ private handleError(error: unknown) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text' as const,
62
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
68
+
69
+ private setupTools() {
70
+ // Get cards from a specific list
71
+ this.server.registerTool(
72
+ 'get_cards_by_list_id',
73
+ {
74
+ title: 'Get Cards by List ID',
75
+ description:
76
+ 'Fetch cards from a specific Trello list on a specific board. Descriptions are previewed by default to keep responses compact; set fields without "desc" to omit descriptions, or increase descMaxLength/omitDescThresholdBytes and use get_card for full details.',
77
+ inputSchema: {
78
+ boardId: z
79
+ .string()
80
+ .optional()
81
+ .describe('ID of the Trello board (uses default if not provided)'),
82
+ listId: z.string().describe('ID of the Trello list'),
83
+ fields: z
84
+ .string()
85
+ .optional()
86
+ .describe('Comma-separated list of fields to return (e.g., "name,idShort,labels,due,dueComplete"). Omit for all fields.'),
87
+ nameFilter: z
88
+ .string()
89
+ .trim()
90
+ .min(1, 'nameFilter must not be empty')
91
+ .optional()
92
+ .describe('Optional substring to filter cards by name (case-insensitive)'),
93
+ descMaxLength: z
94
+ .number()
95
+ .int()
96
+ .min(0)
97
+ .optional()
98
+ .describe(
99
+ 'Maximum description preview length per card. Defaults to 200. Increase for fuller descriptions.'
100
+ ),
101
+ omitDescThresholdBytes: z
102
+ .number()
103
+ .int()
104
+ .positive()
105
+ .optional()
106
+ .describe(
107
+ 'Approximate response size threshold before descriptions are omitted. Defaults to 50000 bytes.'
108
+ ),
109
+ },
110
+ },
111
+ async ({ listId, fields, nameFilter, descMaxLength, omitDescThresholdBytes }) => {
112
+ try {
113
+ const cards = await this.trelloClient.getCardsByList(listId, fields, nameFilter);
114
+ return formatCardListResponse(cards, { descMaxLength, omitDescThresholdBytes });
115
+ } catch (error) {
116
+ return this.handleError(error);
117
+ }
118
+ }
119
+ );
120
+
121
+ // Get all lists from a board
122
+ this.server.registerTool(
123
+ 'get_lists',
124
+ {
125
+ title: 'Get Lists',
126
+ description: 'Retrieve all lists from the specified board',
127
+ inputSchema: {
128
+ boardId: z
129
+ .string()
130
+ .optional()
131
+ .describe('ID of the Trello board (uses default if not provided)'),
132
+ },
133
+ },
134
+ async ({ boardId }) => {
135
+ try {
136
+ const lists = await this.trelloClient.getLists(boardId);
137
+ return {
138
+ content: [{ type: 'text' as const, text: JSON.stringify(lists, null, 2) }],
139
+ };
140
+ } catch (error) {
141
+ return this.handleError(error);
142
+ }
143
+ }
144
+ );
145
+
146
+ // Get recent activity
147
+ this.server.registerTool(
148
+ 'get_recent_activity',
149
+ {
150
+ title: 'Get Recent Activity',
151
+ description: 'Fetch recent activity on the Trello board',
152
+ inputSchema: {
153
+ boardId: z
154
+ .string()
155
+ .optional()
156
+ .describe('ID of the Trello board (uses default if not provided)'),
157
+ limit: z
158
+ .number()
159
+ .optional()
160
+ .default(10)
161
+ .describe('Number of activities to fetch (default: 10)'),
162
+ since: z
163
+ .string()
164
+ .optional()
165
+ .describe('Only return actions after this date (ISO 8601) or action ID'),
166
+ before: z
167
+ .string()
168
+ .optional()
169
+ .describe('Only return actions before this date (ISO 8601) or action ID'),
170
+ },
171
+ },
172
+ async ({ boardId, limit, since, before }) => {
173
+ try {
174
+ const activity = await this.trelloClient.getRecentActivity(boardId, limit, since, before);
175
+ return {
176
+ content: [{ type: 'text' as const, text: JSON.stringify(activity, null, 2) }],
177
+ };
178
+ } catch (error) {
179
+ return this.handleError(error);
180
+ }
181
+ }
182
+ );
183
+
184
+ // Add a new card to a list
185
+ this.server.registerTool(
186
+ 'add_card_to_list',
187
+ {
188
+ title: 'Add Card to List',
189
+ description: 'Add a new card to a specified list on a specific board',
190
+ inputSchema: {
191
+ boardId: z
192
+ .string()
193
+ .optional()
194
+ .describe('ID of the Trello board (uses default if not provided)'),
195
+ listId: z.string().describe('ID of the list to add the card to'),
196
+ name: z.string().describe('Name of the card'),
197
+ description: z.string().optional().describe('Description of the card'),
198
+ dueDate: z.string().optional().describe('Due date for the card (ISO 8601 format)'),
199
+ dueReminder: z
200
+ .number()
201
+ .int()
202
+ .nullable()
203
+ .optional()
204
+ .describe(
205
+ 'Due date reminder in minutes before due date (e.g., null to remove reminder, 0 at due time, 1440 one day before)'
206
+ ),
207
+ start: z
208
+ .string()
209
+ .optional()
210
+ .describe('Start date for the card (YYYY-MM-DD format, date only)'),
211
+ labels: z
212
+ .array(z.string())
213
+ .optional()
214
+ .describe('Array of label IDs to apply to the card'),
215
+ },
216
+ },
217
+ async args => {
218
+ try {
219
+ const card = await this.trelloClient.addCard(args.boardId, args);
220
+ return {
221
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
222
+ };
223
+ } catch (error) {
224
+ return this.handleError(error);
225
+ }
226
+ }
227
+ );
228
+
229
+ // Update card details
230
+ this.server.registerTool(
231
+ 'update_card_details',
232
+ {
233
+ title: 'Update Card Details',
234
+ description: "Update an existing card's details on a specific board",
235
+ inputSchema: {
236
+ boardId: z
237
+ .string()
238
+ .optional()
239
+ .describe('ID of the Trello board (uses default if not provided)'),
240
+ cardId: z.string().describe('ID of the card to update'),
241
+ name: z.string().optional().describe('New name for the card'),
242
+ description: z.string().optional().describe('New description for the card'),
243
+ dueDate: z.string().optional().describe('New due date for the card (ISO 8601 format)'),
244
+ dueReminder: z
245
+ .number()
246
+ .int()
247
+ .nullable()
248
+ .optional()
249
+ .describe(
250
+ 'New due date reminder in minutes before due date (e.g., null to remove reminder, 0 at due time, 1440 one day before)'
251
+ ),
252
+ start: z
253
+ .string()
254
+ .optional()
255
+ .describe('New start date for the card (YYYY-MM-DD format, date only)'),
256
+ dueComplete: z
257
+ .boolean()
258
+ .optional()
259
+ .describe('Mark the due date as complete (true) or incomplete (false)'),
260
+ labels: z.array(z.string()).optional().describe('New array of label IDs for the card'),
261
+ pos: z
262
+ .union([z.string(), z.number()])
263
+ .optional()
264
+ .describe(
265
+ 'Position of the card in the list. Accepts "top", "bottom", or a positive number'
266
+ ),
267
+ },
268
+ },
269
+ async args => {
270
+ try {
271
+ const card = await this.trelloClient.updateCard(args.boardId, args);
272
+ return {
273
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
274
+ };
275
+ } catch (error) {
276
+ return this.handleError(error);
277
+ }
278
+ }
279
+ );
280
+
281
+ // Archive a card
282
+ this.server.registerTool(
283
+ 'archive_card',
284
+ {
285
+ title: 'Archive Card',
286
+ description: 'Send a card to the archive on a specific board',
287
+ inputSchema: {
288
+ boardId: z
289
+ .string()
290
+ .optional()
291
+ .describe('ID of the Trello board (uses default if not provided)'),
292
+ cardId: z.string().describe('ID of the card to archive'),
293
+ },
294
+ },
295
+ async ({ boardId, cardId }) => {
296
+ try {
297
+ const card = await this.trelloClient.archiveCard(boardId, cardId);
298
+ return {
299
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
300
+ };
301
+ } catch (error) {
302
+ return this.handleError(error);
303
+ }
304
+ }
305
+ );
306
+
307
+ // ─── Watch Card (subscribe/unsubscribe) ──
308
+ this.server.registerTool(
309
+ 'watch_card',
310
+ {
311
+ title: 'Watch Card',
312
+ description: 'Subscribe or unsubscribe from watching a card for activity notifications',
313
+ inputSchema: {
314
+ cardId: z.string().describe('ID of the card to watch/unwatch'),
315
+ subscribed: z.boolean().describe('Set to true to start watching, false to stop'),
316
+ },
317
+ },
318
+ async ({ cardId, subscribed }) => {
319
+ try {
320
+ const card = await this.trelloClient.watchCard(cardId, subscribed);
321
+ return {
322
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
323
+ };
324
+ } catch (error) {
325
+ return this.handleError(error);
326
+ }
327
+ }
328
+ );
329
+
330
+ // ─── Watch List (subscribe/unsubscribe) ──
331
+ this.server.registerTool(
332
+ 'watch_list',
333
+ {
334
+ title: 'Watch List',
335
+ description: 'Subscribe or unsubscribe from watching a list for activity notifications',
336
+ inputSchema: {
337
+ listId: z.string().describe('ID of the list to watch/unwatch'),
338
+ subscribed: z.boolean().describe('Set to true to start watching, false to stop'),
339
+ },
340
+ },
341
+ async ({ listId, subscribed }) => {
342
+ try {
343
+ const list = await this.trelloClient.watchList(listId, subscribed);
344
+ return {
345
+ content: [{ type: 'text' as const, text: JSON.stringify(list, null, 2) }],
346
+ };
347
+ } catch (error) {
348
+ return this.handleError(error);
349
+ }
350
+ }
351
+ );
352
+
353
+ // Move a card
354
+ this.server.registerTool(
355
+ 'move_card',
356
+ {
357
+ title: 'Move Card',
358
+ description: 'Move a card to a different list, potentially on a different board',
359
+ inputSchema: {
360
+ boardId: z
361
+ .string()
362
+ .optional()
363
+ .describe(
364
+ 'ID of the target Trello board (where the listId resides, uses default if not provided)'
365
+ ),
366
+ cardId: z.string().describe('ID of the card to move'),
367
+ listId: z.string().describe('ID of the target list'),
368
+ pos: z
369
+ .union([z.string(), z.number()])
370
+ .optional()
371
+ .describe(
372
+ 'Position of the card in the target list. Accepts "top", "bottom", or a positive number'
373
+ ),
374
+ },
375
+ },
376
+ async ({ boardId, cardId, listId, pos }) => {
377
+ try {
378
+ const card = await this.trelloClient.moveCard(boardId, cardId, listId, pos);
379
+ return {
380
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
381
+ };
382
+ } catch (error) {
383
+ return this.handleError(error);
384
+ }
385
+ }
386
+ );
387
+
388
+ // Add a new list to a board
389
+ this.server.registerTool(
390
+ 'add_list_to_board',
391
+ {
392
+ title: 'Add List to Board',
393
+ description: 'Add a new list to the specified board',
394
+ inputSchema: {
395
+ boardId: z
396
+ .string()
397
+ .optional()
398
+ .describe('ID of the Trello board (uses default if not provided)'),
399
+ name: z.string().describe('Name of the new list'),
400
+ },
401
+ },
402
+ async ({ boardId, name }) => {
403
+ try {
404
+ const list = await this.trelloClient.addList(boardId, name);
405
+ return {
406
+ content: [{ type: 'text' as const, text: JSON.stringify(list, null, 2) }],
407
+ };
408
+ } catch (error) {
409
+ return this.handleError(error);
410
+ }
411
+ }
412
+ );
413
+
414
+ // Archive a list
415
+ this.server.registerTool(
416
+ 'archive_list',
417
+ {
418
+ title: 'Archive List',
419
+ description: 'Send a list to the archive on a specific board',
420
+ inputSchema: {
421
+ boardId: z
422
+ .string()
423
+ .optional()
424
+ .describe('ID of the Trello board (uses default if not provided)'),
425
+ listId: z.string().describe('ID of the list to archive'),
426
+ },
427
+ },
428
+ async ({ boardId, listId }) => {
429
+ try {
430
+ const list = await this.trelloClient.archiveList(boardId, listId);
431
+ return {
432
+ content: [{ type: 'text' as const, text: JSON.stringify(list, null, 2) }],
433
+ };
434
+ } catch (error) {
435
+ return this.handleError(error);
436
+ }
437
+ }
438
+ );
439
+
440
+ // Update a list
441
+ this.server.registerTool(
442
+ 'update_list',
443
+ {
444
+ title: 'Update List',
445
+ description:
446
+ 'Update a list name, archive state, subscription state, or board. Use update_list_position for moving a list within a board.',
447
+ inputSchema: {
448
+ listId: z.string().describe('ID of the Trello list to update'),
449
+ name: z.string().optional().describe('New name for the list'),
450
+ closed: z.boolean().optional().describe('Whether to close (archive) the list'),
451
+ subscribed: z
452
+ .boolean()
453
+ .optional()
454
+ .describe('Whether the authenticated user is subscribed to the list'),
455
+ idBoard: z.string().optional().describe('ID of a board to move the list to'),
456
+ },
457
+ },
458
+ async ({ listId, name, closed, subscribed, idBoard }) => {
459
+ try {
460
+ const params: {
461
+ name?: string;
462
+ closed?: boolean;
463
+ subscribed?: boolean;
464
+ idBoard?: string;
465
+ } = {};
466
+ if (name !== undefined) params.name = name;
467
+ if (closed !== undefined) params.closed = closed;
468
+ if (subscribed !== undefined) params.subscribed = subscribed;
469
+ if (idBoard !== undefined) params.idBoard = idBoard;
470
+ if (Object.keys(params).length === 0) {
471
+ throw new McpError(
472
+ ErrorCode.InvalidParams,
473
+ 'At least one of name, closed, subscribed, or idBoard must be provided'
474
+ );
475
+ }
476
+ const list = await this.trelloClient.updateList(listId, params);
477
+ return {
478
+ content: [{ type: 'text' as const, text: JSON.stringify(list, null, 2) }],
479
+ };
480
+ } catch (error) {
481
+ return this.handleError(error);
482
+ }
483
+ }
484
+ );
485
+
486
+ // Update list position
487
+ this.server.registerTool(
488
+ 'update_list_position',
489
+ {
490
+ title: 'Update List Position',
491
+ description:
492
+ 'Update the position of a list on the board. Trello uses fractional indexing: each list has a float position, and to place a list between two others, use the average of their positions (e.g., between pos 1024 and 2048, use 1536). Use "top"/"bottom" shortcuts to move to the edges.',
493
+ inputSchema: {
494
+ listId: z.string().describe('ID of the list to reposition'),
495
+ position: z
496
+ .string()
497
+ .refine(
498
+ (val) => {
499
+ if (val === 'top' || val === 'bottom') return true;
500
+ const num = Number(val);
501
+ return num > 0 && isFinite(num);
502
+ },
503
+ {
504
+ message: "Position must be 'top', 'bottom', or a positive finite numeric string.",
505
+ }
506
+ )
507
+ .describe(
508
+ 'New position: "top" (move to leftmost), "bottom" (move to rightmost), or a numeric string (e.g. "1536"). To place between two lists, use the average of their pos values.'
509
+ ),
510
+ },
511
+ },
512
+ async ({ listId, position }) => {
513
+ try {
514
+ const parsedPosition =
515
+ position === 'top' || position === 'bottom' ? position : Number(position);
516
+ const list = await this.trelloClient.updateListPosition(listId, parsedPosition);
517
+ return {
518
+ content: [{ type: 'text' as const, text: JSON.stringify(list, null, 2) }],
519
+ };
520
+ } catch (error) {
521
+ return this.handleError(error);
522
+ }
523
+ }
524
+ );
525
+
526
+ // Get cards assigned to current user
527
+ this.server.registerTool(
528
+ 'get_my_cards',
529
+ {
530
+ title: 'Get My Cards',
531
+ description: 'Fetch all cards assigned to the current user',
532
+ inputSchema: {},
533
+ },
534
+ async () => {
535
+ try {
536
+ const cards = await this.trelloClient.getMyCards();
537
+ return {
538
+ content: [{ type: 'text' as const, text: JSON.stringify(cards, null, 2) }],
539
+ };
540
+ } catch (error) {
541
+ return this.handleError(error);
542
+ }
543
+ }
544
+ );
545
+
546
+ // Attach image to card (kept for backward compatibility)
547
+ this.server.registerTool(
548
+ 'attach_image_to_card',
549
+ {
550
+ title: 'Attach Image to Card',
551
+ description: 'Attach an image to a card from a URL on a specific board',
552
+ inputSchema: {
553
+ boardId: z
554
+ .string()
555
+ .optional()
556
+ .describe(
557
+ 'ID of the Trello board where the card exists (uses default if not provided)'
558
+ ),
559
+ cardId: z.string().describe('ID of the card to attach the image to'),
560
+ imageUrl: z.string().describe('URL of the image to attach'),
561
+ name: z
562
+ .string()
563
+ .optional()
564
+ .default('Image Attachment')
565
+ .describe('Optional name for the attachment (defaults to "Image Attachment")'),
566
+ },
567
+ },
568
+ async ({ boardId, cardId, imageUrl, name }) => {
569
+ try {
570
+ const attachment = await this.trelloClient.attachImageToCard(
571
+ boardId,
572
+ cardId,
573
+ imageUrl,
574
+ name
575
+ );
576
+ return {
577
+ content: [{ type: 'text' as const, text: JSON.stringify(attachment, null, 2) }],
578
+ };
579
+ } catch (error) {
580
+ return this.handleError(error);
581
+ }
582
+ }
583
+ );
584
+
585
+ // Attach file to card (generic file attachment)
586
+ this.server.registerTool(
587
+ 'attach_file_to_card',
588
+ {
589
+ title: 'Attach File to Card',
590
+ description: 'Attach any file to a card from a URL on a specific board',
591
+ inputSchema: {
592
+ boardId: z
593
+ .string()
594
+ .optional()
595
+ .describe(
596
+ 'ID of the Trello board where the card exists (uses default if not provided)'
597
+ ),
598
+ cardId: z.string().describe('ID of the card to attach the file to'),
599
+ fileUrl: z.string().describe('URL of the file to attach'),
600
+ name: z
601
+ .string()
602
+ .optional()
603
+ .default('File Attachment')
604
+ .describe('Optional name for the attachment (defaults to "File Attachment")'),
605
+ mimeType: z
606
+ .string()
607
+ .optional()
608
+ .describe(
609
+ 'Optional MIME type of the file (e.g., "application/pdf", "text/plain", "video/mp4")'
610
+ ),
611
+ },
612
+ },
613
+ async ({ boardId, cardId, fileUrl, name, mimeType }) => {
614
+ try {
615
+ const attachment = await this.trelloClient.attachFileToCard(
616
+ boardId,
617
+ cardId,
618
+ fileUrl,
619
+ name,
620
+ mimeType
621
+ );
622
+ return {
623
+ content: [{ type: 'text' as const, text: JSON.stringify(attachment, null, 2) }],
624
+ };
625
+ } catch (error) {
626
+ return {
627
+ content: [
628
+ {
629
+ type: 'text' as const,
630
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
631
+ },
632
+ ],
633
+ isError: true,
634
+ };
635
+ }
636
+ }
637
+ );
638
+
639
+ // Attach arbitrary binary data to a card (base64 or data URL)
640
+ this.server.registerTool(
641
+ 'attach_data_to_card',
642
+ {
643
+ title: 'Attach Data to Card',
644
+ description:
645
+ 'Attach binary data (image, markdown, PDF, text, etc.) to a card from base64-encoded data or a data URL. Use this for any non-image content. For image/screenshot uploads with PNG defaults, see attach_image_data_to_card.',
646
+ inputSchema: {
647
+ boardId: z
648
+ .string()
649
+ .optional()
650
+ .describe(
651
+ 'ID of the Trello board where the card exists (uses default if not provided)'
652
+ ),
653
+ cardId: z.string().describe('ID of the card to attach the data to'),
654
+ data: z
655
+ .string()
656
+ .describe(
657
+ 'Base64-encoded data or a data URL (e.g. data:text/markdown;base64,...). Any content type, not just images.'
658
+ ),
659
+ name: z
660
+ .string()
661
+ .optional()
662
+ .describe(
663
+ 'Filename for the attachment, including extension (e.g. "notes.md", "report.pdf"). Defaults to "attachment-<timestamp>".'
664
+ ),
665
+ mimeType: z
666
+ .string()
667
+ .optional()
668
+ .describe(
669
+ 'MIME type of the data (e.g. "text/markdown", "application/pdf", "image/png"). Recommended for correct rendering in Trello. If omitted, inferred from a data URL prefix or the filename extension; falls back to "application/octet-stream".'
670
+ ),
671
+ },
672
+ },
673
+ async ({ boardId, cardId, data, name, mimeType }) => {
674
+ try {
675
+ const attachment = await this.trelloClient.attachDataToCard(
676
+ boardId,
677
+ cardId,
678
+ data,
679
+ name,
680
+ mimeType
681
+ );
682
+ return {
683
+ content: [{ type: 'text' as const, text: JSON.stringify(attachment, null, 2) }],
684
+ };
685
+ } catch (error) {
686
+ return this.handleError(error);
687
+ }
688
+ }
689
+ );
690
+
691
+ // Attach image data to card (image-flavored convenience over attach_data_to_card)
692
+ this.server.registerTool(
693
+ 'attach_image_data_to_card',
694
+ {
695
+ title: 'Attach Image Data to Card',
696
+ description:
697
+ 'Attach an image to a card from base64 data or a data URL. Image-flavored convenience over attach_data_to_card: defaults assume PNG when mimeType/name are omitted, suitable for screenshot pasting. For non-image content, use attach_data_to_card.',
698
+ inputSchema: {
699
+ boardId: z
700
+ .string()
701
+ .optional()
702
+ .describe(
703
+ 'ID of the Trello board where the card exists (uses default if not provided)'
704
+ ),
705
+ cardId: z.string().describe('ID of the card to attach the image to'),
706
+ imageData: z.string().describe('Base64 encoded image data or data URL (e.g., data:image/png;base64,...)'),
707
+ name: z
708
+ .string()
709
+ .optional()
710
+ .describe('Optional name for the attachment'),
711
+ mimeType: z
712
+ .string()
713
+ .optional()
714
+ .default('image/png')
715
+ .describe('Optional MIME type (default: image/png)'),
716
+ },
717
+ },
718
+ async ({ boardId, cardId, imageData, name, mimeType }) => {
719
+ try {
720
+ const attachment = await this.trelloClient.attachImageDataToCard(
721
+ boardId,
722
+ cardId,
723
+ imageData,
724
+ name,
725
+ mimeType
726
+ );
727
+ return {
728
+ content: [{ type: 'text' as const, text: JSON.stringify(attachment, null, 2) }],
729
+ };
730
+ } catch (error) {
731
+ return this.handleError(error);
732
+ }
733
+ }
734
+ );
735
+
736
+ // List all boards
737
+ this.server.registerTool(
738
+ 'list_boards',
739
+ {
740
+ title: 'List Boards',
741
+ description: 'List all boards the user has access to',
742
+ inputSchema: {},
743
+ },
744
+ async () => {
745
+ try {
746
+ const boards = await this.trelloClient.listBoards();
747
+ return {
748
+ content: [{ type: 'text' as const, text: JSON.stringify(boards, null, 2) }],
749
+ };
750
+ } catch (error) {
751
+ return this.handleError(error);
752
+ }
753
+ }
754
+ );
755
+
756
+ // Set active board
757
+ this.server.registerTool(
758
+ 'set_active_board',
759
+ {
760
+ title: 'Set Active Board',
761
+ description: 'Set the active board for future operations',
762
+ inputSchema: {
763
+ boardId: z.string().describe('ID of the board to set as active'),
764
+ },
765
+ },
766
+ async ({ boardId }) => {
767
+ try {
768
+ const board = await this.trelloClient.setActiveBoard(boardId);
769
+ return {
770
+ content: [
771
+ {
772
+ type: 'text' as const,
773
+ text: `Successfully set active board to "${board.name}" (${board.id})`,
774
+ },
775
+ ],
776
+ };
777
+ } catch (error) {
778
+ return this.handleError(error);
779
+ }
780
+ }
781
+ );
782
+
783
+ // List workspaces
784
+ this.server.registerTool(
785
+ 'list_workspaces',
786
+ {
787
+ title: 'List Workspaces',
788
+ description:
789
+ 'List workspaces the user has access to. If TRELLO_ALLOWED_WORKSPACES is configured, only allowed workspaces are returned.',
790
+ inputSchema: {},
791
+ },
792
+ async () => {
793
+ try {
794
+ const workspaces = await this.trelloClient.listWorkspaces();
795
+ return {
796
+ content: [{ type: 'text' as const, text: JSON.stringify(workspaces, null, 2) }],
797
+ };
798
+ } catch (error) {
799
+ return this.handleError(error);
800
+ }
801
+ }
802
+ );
803
+
804
+ // Create a new board
805
+ this.server.registerTool(
806
+ 'create_board',
807
+ {
808
+ title: 'Create Board',
809
+ description: 'Create a new Trello board optionally within a workspace',
810
+ inputSchema: {
811
+ name: z.string().describe('Name of the board'),
812
+ desc: z.string().optional().describe('Description of the board'),
813
+ idOrganization: z
814
+ .string()
815
+ .min(1)
816
+ .optional()
817
+ .describe('Workspace ID to create the board in (uses active if not provided)'),
818
+ defaultLabels: z
819
+ .boolean()
820
+ .optional()
821
+ .default(true)
822
+ .describe('Create default labels (true by default)'),
823
+ defaultLists: z
824
+ .boolean()
825
+ .optional()
826
+ .default(true)
827
+ .describe('Create default lists (true by default)'),
828
+ },
829
+ },
830
+ async ({ name, desc, idOrganization, defaultLabels, defaultLists }) => {
831
+ try {
832
+ const board = await this.trelloClient.createBoard({
833
+ name,
834
+ desc,
835
+ idOrganization,
836
+ defaultLabels,
837
+ defaultLists,
838
+ });
839
+ return {
840
+ content: [{ type: 'text' as const, text: JSON.stringify(board, null, 2) }],
841
+ };
842
+ } catch (error) {
843
+ return this.handleError(error);
844
+ }
845
+ }
846
+ );
847
+
848
+ // Set active workspace
849
+ this.server.registerTool(
850
+ 'set_active_workspace',
851
+ {
852
+ title: 'Set Active Workspace',
853
+ description: 'Set the active workspace for future operations',
854
+ inputSchema: {
855
+ workspaceId: z.string().describe('ID of the workspace to set as active'),
856
+ },
857
+ },
858
+ async ({ workspaceId }) => {
859
+ try {
860
+ const workspace = await this.trelloClient.setActiveWorkspace(workspaceId);
861
+ return {
862
+ content: [
863
+ {
864
+ type: 'text' as const,
865
+ text: `Successfully set active workspace to "${workspace.displayName}" (${workspace.id})`,
866
+ },
867
+ ],
868
+ };
869
+ } catch (error) {
870
+ return this.handleError(error);
871
+ }
872
+ }
873
+ );
874
+
875
+ // List boards in workspace
876
+ this.server.registerTool(
877
+ 'list_boards_in_workspace',
878
+ {
879
+ title: 'List Boards in Workspace',
880
+ description: 'List all boards in a specific workspace',
881
+ inputSchema: {
882
+ workspaceId: z.string().describe('ID of the workspace to list boards from'),
883
+ },
884
+ },
885
+ async ({ workspaceId }) => {
886
+ try {
887
+ const boards = await this.trelloClient.listBoardsInWorkspace(workspaceId);
888
+ return {
889
+ content: [{ type: 'text' as const, text: JSON.stringify(boards, null, 2) }],
890
+ };
891
+ } catch (error) {
892
+ return this.handleError(error);
893
+ }
894
+ }
895
+ );
896
+
897
+ // Get active board info
898
+ this.server.registerTool(
899
+ 'get_active_board_info',
900
+ {
901
+ title: 'Get Active Board Info',
902
+ description: 'Get information about the currently active board',
903
+ inputSchema: {},
904
+ },
905
+ async () => {
906
+ try {
907
+ const boardId = this.trelloClient.activeBoardId;
908
+ if (!boardId) {
909
+ return {
910
+ content: [{ type: 'text' as const, text: 'No active board set' }],
911
+ isError: true,
912
+ };
913
+ }
914
+ const board = await this.trelloClient.getBoardById(boardId);
915
+ return {
916
+ content: [
917
+ {
918
+ type: 'text' as const,
919
+ text: JSON.stringify(
920
+ {
921
+ ...board,
922
+ isActive: true,
923
+ activeWorkspaceId: this.trelloClient.activeWorkspaceId || 'Not set',
924
+ },
925
+ null,
926
+ 2
927
+ ),
928
+ },
929
+ ],
930
+ };
931
+ } catch (error) {
932
+ return this.handleError(error);
933
+ }
934
+ }
935
+ );
936
+
937
+ // Get card details
938
+ this.server.registerTool(
939
+ 'get_card',
940
+ {
941
+ title: 'Get Card',
942
+ description: 'Get detailed information about a specific Trello card',
943
+ inputSchema: {
944
+ cardId: z.string().describe('ID of the card to fetch'),
945
+ includeMarkdown: z
946
+ .boolean()
947
+ .optional()
948
+ .default(false)
949
+ .describe('Whether to return card description in markdown format (default: false)'),
950
+ },
951
+ },
952
+ async ({ cardId, includeMarkdown }) => {
953
+ try {
954
+ const card = await this.trelloClient.getCard(cardId, includeMarkdown);
955
+ return {
956
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
957
+ };
958
+ } catch (error) {
959
+ return this.handleError(error);
960
+ }
961
+ }
962
+ );
963
+
964
+ // Add a comment to a card
965
+ this.server.registerTool(
966
+ 'add_comment',
967
+ {
968
+ title: 'Add Comment to Card',
969
+ description: 'Add the given text as a new comment to the given card',
970
+ inputSchema: {
971
+ cardId: z.string().describe('ID of the card to comment on'),
972
+ text: z.string().describe('The text of the comment to add'),
973
+ },
974
+ },
975
+ async ({ cardId, text }) => {
976
+ try {
977
+ const comment = await this.trelloClient.addCommentToCard(cardId, text);
978
+ return {
979
+ content: [{ type: 'text' as const, text: JSON.stringify(comment, null, 2) }],
980
+ };
981
+ } catch (error) {
982
+ return this.handleError(error);
983
+ }
984
+ }
985
+ );
986
+
987
+ // Update a comment to a card
988
+ this.server.registerTool(
989
+ 'update_comment',
990
+ {
991
+ title: 'Update Comment on Card',
992
+ description: 'Update the given comment with the new text',
993
+ inputSchema: {
994
+ commentId: z.string().describe('ID of the comment to change'),
995
+ text: z.string().describe('The new text of the comment'),
996
+ },
997
+ },
998
+ async ({ commentId, text }) => {
999
+ try {
1000
+ const success = await this.trelloClient.updateCommentOnCard(commentId, text);
1001
+ return {
1002
+ content: [{ type: 'text' as const, text: success ? 'success' : 'failure' }],
1003
+ };
1004
+ } catch (error) {
1005
+ return this.handleError(error);
1006
+ }
1007
+ }
1008
+ );
1009
+
1010
+ // Delete a comment from a card
1011
+ this.server.registerTool(
1012
+ 'delete_comment',
1013
+ {
1014
+ title: 'Delete Comment from Card',
1015
+ description: 'Delete a comment from a Trello card',
1016
+ inputSchema: {
1017
+ commentId: z.string().describe('ID of the comment to delete'),
1018
+ },
1019
+ },
1020
+ async ({ commentId }) => {
1021
+ try {
1022
+ const success = await this.trelloClient.deleteCommentFromCard(commentId);
1023
+ return {
1024
+ content: [{ type: 'text' as const, text: success ? 'success' : 'failure' }],
1025
+ };
1026
+ } catch (error) {
1027
+ return this.handleError(error);
1028
+ }
1029
+ }
1030
+ );
1031
+
1032
+ // Get comments from a card
1033
+ this.server.registerTool(
1034
+ 'get_card_comments',
1035
+ {
1036
+ title: 'Get Card Comments',
1037
+ description: 'Retrieve all comments from a specific Trello card',
1038
+ inputSchema: {
1039
+ cardId: z.string().describe('ID of the card to get comments from'),
1040
+ limit: z
1041
+ .number()
1042
+ .optional()
1043
+ .default(100)
1044
+ .describe('Maximum number of comments to retrieve (default: 100)'),
1045
+ },
1046
+ },
1047
+ async ({ cardId, limit }) => {
1048
+ try {
1049
+ const comments = await this.trelloClient.getCardComments(cardId, limit);
1050
+ return {
1051
+ content: [{ type: 'text' as const, text: JSON.stringify(comments, null, 2) }],
1052
+ };
1053
+ } catch (error) {
1054
+ return this.handleError(error);
1055
+ }
1056
+ }
1057
+ );
1058
+
1059
+ // Checklist tools
1060
+ this.server.registerTool(
1061
+ 'create_checklist',
1062
+ {
1063
+ title: 'Create Checklist',
1064
+ description: 'Create a new checklist',
1065
+ inputSchema: {
1066
+ name: z.string().describe('Name of the checklist to create'),
1067
+ cardId: z.string().describe('ID of the Trello card'),
1068
+ },
1069
+ },
1070
+ async ({ name, cardId }) => {
1071
+ try {
1072
+ const items = await this.trelloClient.createChecklist(name, cardId);
1073
+ return {
1074
+ content: [{ type: 'text' as const, text: JSON.stringify(items, null, 2) }],
1075
+ };
1076
+ } catch (error) {
1077
+ return this.handleError(error);
1078
+ }
1079
+ }
1080
+ );
1081
+
1082
+ // Checklist tools
1083
+ this.server.registerTool(
1084
+ 'get_checklist_items',
1085
+ {
1086
+ title: 'Get Checklist Items',
1087
+ description: 'Get all items from a checklist by name',
1088
+ inputSchema: {
1089
+ name: z.string().describe('Name of the checklist to retrieve items from'),
1090
+ cardId: z
1091
+ .string()
1092
+ .optional()
1093
+ .describe('ID of the card to scope checklist search to (recommended to avoid ambiguity)'),
1094
+ boardId: z
1095
+ .string()
1096
+ .optional()
1097
+ .describe('ID of the Trello board (uses default if not provided)'),
1098
+ },
1099
+ },
1100
+ async ({ name, cardId, boardId }) => {
1101
+ try {
1102
+ const items = await this.trelloClient.getChecklistItems(name, cardId, boardId);
1103
+ return {
1104
+ content: [{ type: 'text' as const, text: JSON.stringify(items, null, 2) }],
1105
+ };
1106
+ } catch (error) {
1107
+ return this.handleError(error);
1108
+ }
1109
+ }
1110
+ );
1111
+
1112
+ this.server.registerTool(
1113
+ 'add_checklist_item',
1114
+ {
1115
+ title: 'Add Checklist Item',
1116
+ description: 'Add a new item to a checklist',
1117
+ inputSchema: {
1118
+ text: z.string().describe('Text content of the checklist item'),
1119
+ checkListName: z.string().describe('Name of the checklist to add the item to'),
1120
+ cardId: z
1121
+ .string()
1122
+ .optional()
1123
+ .describe('ID of the card to scope checklist search to (recommended to avoid ambiguity)'),
1124
+ boardId: z
1125
+ .string()
1126
+ .optional()
1127
+ .describe('ID of the Trello board (uses default if not provided)'),
1128
+ },
1129
+ },
1130
+ async ({ text, checkListName, cardId, boardId }) => {
1131
+ try {
1132
+ const item = await this.trelloClient.addChecklistItem(text, checkListName, cardId, boardId);
1133
+ return {
1134
+ content: [{ type: 'text' as const, text: JSON.stringify(item, null, 2) }],
1135
+ };
1136
+ } catch (error) {
1137
+ return this.handleError(error);
1138
+ }
1139
+ }
1140
+ );
1141
+
1142
+ this.server.registerTool(
1143
+ 'find_checklist_items_by_description',
1144
+ {
1145
+ title: 'Find Checklist Items by Description',
1146
+ description: 'Search for checklist items containing specific text in their description',
1147
+ inputSchema: {
1148
+ description: z.string().describe('Text to search for in checklist item descriptions'),
1149
+ cardId: z
1150
+ .string()
1151
+ .optional()
1152
+ .describe('ID of the card to scope checklist search to (recommended to avoid ambiguity)'),
1153
+ boardId: z
1154
+ .string()
1155
+ .optional()
1156
+ .describe('ID of the Trello board (uses default if not provided)'),
1157
+ },
1158
+ },
1159
+ async ({ description, cardId, boardId }) => {
1160
+ try {
1161
+ const items = await this.trelloClient.findChecklistItemsByDescription(
1162
+ description,
1163
+ cardId,
1164
+ boardId
1165
+ );
1166
+ return {
1167
+ content: [{ type: 'text' as const, text: JSON.stringify(items, null, 2) }],
1168
+ };
1169
+ } catch (error) {
1170
+ return this.handleError(error);
1171
+ }
1172
+ }
1173
+ );
1174
+
1175
+ this.server.registerTool(
1176
+ 'get_acceptance_criteria',
1177
+ {
1178
+ title: 'Get Acceptance Criteria',
1179
+ description: 'Get all items from the "Acceptance Criteria" checklist',
1180
+ inputSchema: {
1181
+ cardId: z
1182
+ .string()
1183
+ .optional()
1184
+ .describe('ID of the card to scope checklist search to (recommended to avoid ambiguity)'),
1185
+ boardId: z
1186
+ .string()
1187
+ .optional()
1188
+ .describe('ID of the Trello board (uses default if not provided)'),
1189
+ },
1190
+ },
1191
+ async ({ cardId, boardId }) => {
1192
+ try {
1193
+ const items = await this.trelloClient.getAcceptanceCriteria(cardId, boardId);
1194
+ return {
1195
+ content: [{ type: 'text' as const, text: JSON.stringify(items, null, 2) }],
1196
+ };
1197
+ } catch (error) {
1198
+ return this.handleError(error);
1199
+ }
1200
+ }
1201
+ );
1202
+
1203
+ this.server.registerTool(
1204
+ 'get_checklist_by_name',
1205
+ {
1206
+ title: 'Get Checklist by Name',
1207
+ description: 'Get a complete checklist with all its items and completion percentage',
1208
+ inputSchema: {
1209
+ name: z.string().describe('Name of the checklist to retrieve'),
1210
+ cardId: z
1211
+ .string()
1212
+ .optional()
1213
+ .describe('ID of the card to scope checklist search to (recommended to avoid ambiguity)'),
1214
+ boardId: z
1215
+ .string()
1216
+ .optional()
1217
+ .describe('ID of the Trello board (uses default if not provided)'),
1218
+ },
1219
+ },
1220
+ async ({ name, cardId, boardId }) => {
1221
+ try {
1222
+ const checklist = await this.trelloClient.getChecklistByName(name, cardId, boardId);
1223
+ if (!checklist) {
1224
+ return {
1225
+ content: [{ type: 'text' as const, text: `Checklist "${name}" not found` }],
1226
+ isError: true,
1227
+ };
1228
+ }
1229
+ return {
1230
+ content: [{ type: 'text' as const, text: JSON.stringify(checklist, null, 2) }],
1231
+ };
1232
+ } catch (error) {
1233
+ return this.handleError(error);
1234
+ }
1235
+ }
1236
+ );
1237
+
1238
+ this.server.registerTool(
1239
+ 'update_checklist_item',
1240
+ {
1241
+ title: 'Update Checklist Item',
1242
+ description: 'Update a checklist item name, state, position, due date, reminder, or assigned member',
1243
+ inputSchema: {
1244
+ cardId: z.string().describe('ID of the card containing the checklist item'),
1245
+ checkItemId: z.string().describe('ID of the checklist item to update'),
1246
+ state: z
1247
+ .enum(['complete', 'incomplete'])
1248
+ .optional()
1249
+ .describe('New state for the checklist item'),
1250
+ name: z.string().optional().describe('New text for the checklist item'),
1251
+ pos: z
1252
+ .union([z.number(), z.enum(['top', 'bottom'])])
1253
+ .optional()
1254
+ .describe('New position for the checklist item'),
1255
+ due: z
1256
+ .string()
1257
+ .nullable()
1258
+ .optional()
1259
+ .describe('New due date for the checklist item in ISO 8601 format, or null to clear it'),
1260
+ dueReminder: z
1261
+ .number()
1262
+ .nullable()
1263
+ .optional()
1264
+ .describe('Reminder offset in minutes before due date, or null to clear it'),
1265
+ idMember: z
1266
+ .string()
1267
+ .nullable()
1268
+ .optional()
1269
+ .describe('Member ID to assign to the checklist item, or null to clear it'),
1270
+ },
1271
+ },
1272
+ async ({ cardId, checkItemId, name, state, pos, due, dueReminder, idMember }) => {
1273
+ try {
1274
+ const item = await this.trelloClient.updateChecklistItem(cardId, checkItemId, {
1275
+ name,
1276
+ state,
1277
+ pos,
1278
+ due,
1279
+ dueReminder,
1280
+ idMember,
1281
+ });
1282
+ return {
1283
+ content: [{ type: 'text' as const, text: JSON.stringify(item, null, 2) }],
1284
+ };
1285
+ } catch (error) {
1286
+ return this.handleError(error);
1287
+ }
1288
+ }
1289
+ );
1290
+
1291
+ this.server.registerTool(
1292
+ 'delete_checklist_item',
1293
+ {
1294
+ title: 'Delete Checklist Item',
1295
+ description: 'Delete a checklist item from a card',
1296
+ inputSchema: {
1297
+ cardId: z.string().describe('ID of the card containing the checklist item'),
1298
+ checkItemId: z.string().describe('ID of the checklist item to delete'),
1299
+ },
1300
+ },
1301
+ async ({ cardId, checkItemId }) => {
1302
+ try {
1303
+ const deleted = await this.trelloClient.deleteChecklistItem(cardId, checkItemId);
1304
+ return {
1305
+ content: [{ type: 'text' as const, text: JSON.stringify({ deleted }, null, 2) }],
1306
+ };
1307
+ } catch (error) {
1308
+ return this.handleError(error);
1309
+ }
1310
+ }
1311
+ );
1312
+
1313
+ // Member management tools
1314
+ this.server.registerTool(
1315
+ 'get_board_members',
1316
+ {
1317
+ title: 'Get Board Members',
1318
+ description: 'Get all members of a specific board',
1319
+ inputSchema: {
1320
+ boardId: z
1321
+ .string()
1322
+ .optional()
1323
+ .describe('ID of the Trello board (uses default if not provided)'),
1324
+ },
1325
+ },
1326
+ async ({ boardId }) => {
1327
+ try {
1328
+ const members = await this.trelloClient.getBoardMembers(boardId);
1329
+ return {
1330
+ content: [{ type: 'text' as const, text: JSON.stringify(members, null, 2) }],
1331
+ };
1332
+ } catch (error) {
1333
+ return this.handleError(error);
1334
+ }
1335
+ }
1336
+ );
1337
+
1338
+ this.server.registerTool(
1339
+ 'assign_member_to_card',
1340
+ {
1341
+ title: 'Assign Member to Card',
1342
+ description: 'Assign a member to a specific card',
1343
+ inputSchema: {
1344
+ cardId: z.string().describe('ID of the card to assign the member to'),
1345
+ memberId: z.string().describe('ID of the member to assign to the card'),
1346
+ },
1347
+ },
1348
+ async ({ cardId, memberId }) => {
1349
+ try {
1350
+ const card = await this.trelloClient.assignMemberToCard(cardId, memberId);
1351
+ return {
1352
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
1353
+ };
1354
+ } catch (error) {
1355
+ return this.handleError(error);
1356
+ }
1357
+ }
1358
+ );
1359
+
1360
+ this.server.registerTool(
1361
+ 'remove_member_from_card',
1362
+ {
1363
+ title: 'Remove Member from Card',
1364
+ description: 'Remove a member from a specific card',
1365
+ inputSchema: {
1366
+ cardId: z.string().describe('ID of the card to remove the member from'),
1367
+ memberId: z.string().describe('ID of the member to remove from the card'),
1368
+ },
1369
+ },
1370
+ async ({ cardId, memberId }) => {
1371
+ try {
1372
+ const card = await this.trelloClient.removeMemberFromCard(cardId, memberId);
1373
+ return {
1374
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
1375
+ };
1376
+ } catch (error) {
1377
+ return this.handleError(error);
1378
+ }
1379
+ }
1380
+ );
1381
+
1382
+ // Label management tools
1383
+ this.server.registerTool(
1384
+ 'get_board_labels',
1385
+ {
1386
+ title: 'Get Board Labels',
1387
+ description: 'Get all labels of a specific board',
1388
+ inputSchema: {
1389
+ boardId: z
1390
+ .string()
1391
+ .optional()
1392
+ .describe('ID of the Trello board (uses default if not provided)'),
1393
+ },
1394
+ },
1395
+ async ({ boardId }) => {
1396
+ try {
1397
+ const labels = await this.trelloClient.getBoardLabels(boardId);
1398
+ return {
1399
+ content: [{ type: 'text' as const, text: JSON.stringify(labels, null, 2) }],
1400
+ };
1401
+ } catch (error) {
1402
+ return this.handleError(error);
1403
+ }
1404
+ }
1405
+ );
1406
+
1407
+ this.server.registerTool(
1408
+ 'create_label',
1409
+ {
1410
+ title: 'Create Label',
1411
+ description: 'Create a new label on a board',
1412
+ inputSchema: {
1413
+ boardId: z
1414
+ .string()
1415
+ .optional()
1416
+ .describe('ID of the Trello board (uses default if not provided)'),
1417
+ name: z.string().describe('Name of the label'),
1418
+ color: z
1419
+ .string()
1420
+ .optional()
1421
+ .describe(
1422
+ 'Color of the label (e.g., "red", "blue", "green", "yellow", "orange", "purple", "pink", "sky", "lime", "black", "null")'
1423
+ ),
1424
+ },
1425
+ },
1426
+ async ({ boardId, name, color }) => {
1427
+ try {
1428
+ const label = await this.trelloClient.createLabel(boardId, name, color);
1429
+ return {
1430
+ content: [{ type: 'text' as const, text: JSON.stringify(label, null, 2) }],
1431
+ };
1432
+ } catch (error) {
1433
+ return this.handleError(error);
1434
+ }
1435
+ }
1436
+ );
1437
+
1438
+ this.server.registerTool(
1439
+ 'update_label',
1440
+ {
1441
+ title: 'Update Label',
1442
+ description: 'Update an existing label',
1443
+ inputSchema: {
1444
+ labelId: z.string().describe('ID of the label to update'),
1445
+ name: z.string().optional().describe('New name for the label'),
1446
+ color: z.string().optional().describe('New color for the label'),
1447
+ },
1448
+ },
1449
+ async ({ labelId, name, color }) => {
1450
+ try {
1451
+ const label = await this.trelloClient.updateLabel(labelId, name, color);
1452
+ return {
1453
+ content: [{ type: 'text' as const, text: JSON.stringify(label, null, 2) }],
1454
+ };
1455
+ } catch (error) {
1456
+ return this.handleError(error);
1457
+ }
1458
+ }
1459
+ );
1460
+
1461
+ this.server.registerTool(
1462
+ 'delete_label',
1463
+ {
1464
+ title: 'Delete Label',
1465
+ description: 'Delete a label from a board',
1466
+ inputSchema: {
1467
+ labelId: z.string().describe('ID of the label to delete'),
1468
+ },
1469
+ },
1470
+ async ({ labelId }) => {
1471
+ try {
1472
+ await this.trelloClient.deleteLabel(labelId);
1473
+ return {
1474
+ content: [{ type: 'text' as const, text: 'Label deleted successfully' }],
1475
+ };
1476
+ } catch (error) {
1477
+ return this.handleError(error);
1478
+ }
1479
+ }
1480
+ );
1481
+
1482
+ // Copy a card (supports cross-board copy)
1483
+ this.server.registerTool(
1484
+ 'copy_card',
1485
+ {
1486
+ title: 'Copy Card',
1487
+ description:
1488
+ 'Copy/duplicate a Trello card to any list (even on a different board). Copies all properties by default including checklists, attachments, comments, labels, etc.',
1489
+ inputSchema: {
1490
+ sourceCardId: z.string().describe('ID of the source card to copy'),
1491
+ listId: z
1492
+ .string()
1493
+ .describe('ID of the destination list (can be on a different board)'),
1494
+ name: z
1495
+ .string()
1496
+ .optional()
1497
+ .describe('Override the name of the copied card (defaults to source card name)'),
1498
+ description: z
1499
+ .string()
1500
+ .optional()
1501
+ .describe('Override the description of the copied card'),
1502
+ keepFromSource: z
1503
+ .string()
1504
+ .optional()
1505
+ .describe(
1506
+ 'Comma-separated list of properties to copy: "all" (default), or any combination of: attachments, checklists, comments, customFields, due, start, labels, members, stickers'
1507
+ ),
1508
+ pos: z
1509
+ .string()
1510
+ .optional()
1511
+ .describe('Position of the new card: "top", "bottom", or a positive float'),
1512
+ },
1513
+ },
1514
+ async ({ sourceCardId, listId, name, description, keepFromSource, pos }) => {
1515
+ try {
1516
+ const card = await this.trelloClient.copyCard({
1517
+ sourceCardId,
1518
+ listId,
1519
+ name,
1520
+ description,
1521
+ keepFromSource,
1522
+ pos,
1523
+ });
1524
+ return {
1525
+ content: [{ type: 'text' as const, text: JSON.stringify(card, null, 2) }],
1526
+ };
1527
+ } catch (error) {
1528
+ return this.handleError(error);
1529
+ }
1530
+ }
1531
+ );
1532
+
1533
+ // Copy a checklist from one card to another
1534
+ this.server.registerTool(
1535
+ 'copy_checklist',
1536
+ {
1537
+ title: 'Copy Checklist',
1538
+ description:
1539
+ 'Copy a checklist (with all its items) from one card to another. Works across different boards.',
1540
+ inputSchema: {
1541
+ sourceChecklistId: z
1542
+ .string()
1543
+ .describe('ID of the source checklist to copy'),
1544
+ cardId: z
1545
+ .string()
1546
+ .describe('ID of the destination card to copy the checklist to'),
1547
+ name: z
1548
+ .string()
1549
+ .optional()
1550
+ .describe('Override the name of the copied checklist (defaults to source checklist name)'),
1551
+ pos: z
1552
+ .string()
1553
+ .optional()
1554
+ .describe('Position of the new checklist: "top", "bottom", or a positive number'),
1555
+ },
1556
+ },
1557
+ async ({ sourceChecklistId, cardId, name, pos }) => {
1558
+ try {
1559
+ const checklist = await this.trelloClient.copyChecklist({
1560
+ sourceChecklistId,
1561
+ cardId,
1562
+ name,
1563
+ pos,
1564
+ });
1565
+ return {
1566
+ content: [{ type: 'text' as const, text: JSON.stringify(checklist, null, 2) }],
1567
+ };
1568
+ } catch (error) {
1569
+ return this.handleError(error);
1570
+ }
1571
+ }
1572
+ );
1573
+
1574
+ // Add multiple cards to a list
1575
+ this.server.registerTool(
1576
+ 'add_cards_to_list',
1577
+ {
1578
+ title: 'Add Cards to List',
1579
+ description:
1580
+ 'Add multiple cards to a list in one operation. Cards are created sequentially (Trello API does not support batch writes). Rate limiting is handled automatically.',
1581
+ inputSchema: {
1582
+ listId: z.string().describe('ID of the list to add cards to'),
1583
+ cards: z
1584
+ .array(
1585
+ z.object({
1586
+ name: z.string().describe('Name of the card'),
1587
+ description: z.string().optional().describe('Description of the card'),
1588
+ dueDate: z
1589
+ .string()
1590
+ .optional()
1591
+ .describe('Due date for the card (ISO 8601 format)'),
1592
+ start: z
1593
+ .string()
1594
+ .optional()
1595
+ .describe('Start date for the card (YYYY-MM-DD format)'),
1596
+ labels: z
1597
+ .array(z.string())
1598
+ .optional()
1599
+ .describe('Array of label IDs to apply to the card'),
1600
+ })
1601
+ )
1602
+ .describe('Array of cards to create (max 50)'),
1603
+ },
1604
+ },
1605
+ async ({ listId, cards }) => {
1606
+ try {
1607
+ const results = await this.trelloClient.batchAddCards(listId, cards);
1608
+ return {
1609
+ content: [
1610
+ {
1611
+ type: 'text' as const,
1612
+ text: JSON.stringify(results, null, 2),
1613
+ },
1614
+ ],
1615
+ };
1616
+ } catch (error) {
1617
+ return this.handleError(error);
1618
+ }
1619
+ }
1620
+ );
1621
+
1622
+ // Custom field management tools
1623
+ this.server.registerTool(
1624
+ 'get_board_custom_fields',
1625
+ {
1626
+ title: 'Get Board Custom Fields',
1627
+ description:
1628
+ 'Get all custom field definitions on a board. Returns field IDs, names, and types. ' +
1629
+ 'For dropdown/list fields, also returns available options with their IDs. ' +
1630
+ 'Requires Trello Standard plan or higher.',
1631
+ inputSchema: {
1632
+ boardId: z
1633
+ .string()
1634
+ .optional()
1635
+ .describe('ID of the Trello board (uses default if not provided)'),
1636
+ },
1637
+ },
1638
+ async ({ boardId }) => {
1639
+ try {
1640
+ const fields = await this.trelloClient.getBoardCustomFields(boardId);
1641
+
1642
+ const fieldsWithOptions = await Promise.all(
1643
+ fields.map(async (field) => {
1644
+ if (field.type !== 'list') {
1645
+ return field;
1646
+ }
1647
+
1648
+ try {
1649
+ const options = await this.trelloClient.getCustomFieldOptions(field.id);
1650
+ return { ...field, options };
1651
+ } catch (error) {
1652
+ return {
1653
+ ...field,
1654
+ optionsError: error instanceof Error ? error.message : 'Failed to fetch options',
1655
+ };
1656
+ }
1657
+ })
1658
+ );
1659
+
1660
+ return {
1661
+ content: [
1662
+ { type: 'text' as const, text: JSON.stringify(fieldsWithOptions, null, 2) },
1663
+ ],
1664
+ };
1665
+ } catch (error) {
1666
+ return this.handleError(error);
1667
+ }
1668
+ }
1669
+ );
1670
+
1671
+ this.server.registerTool(
1672
+ 'update_card_custom_field',
1673
+ {
1674
+ title: 'Update Card Custom Field',
1675
+ description:
1676
+ 'Set or clear a custom field value on a card. Requires Trello Standard plan or higher. ' +
1677
+ 'Use get_board_custom_fields first to find field IDs and types. ' +
1678
+ 'Value format depends on type: text=any string, number=numeric string, ' +
1679
+ 'checkbox="true"/"false", date=ISO 8601 string, list=option ID from get_board_custom_fields. ' +
1680
+ 'To clear a field, set type to "clear" and omit value.',
1681
+ inputSchema: {
1682
+ cardId: z.string().describe('ID of the card to update'),
1683
+ customFieldId: z.string().describe('ID of the custom field definition'),
1684
+ type: z
1685
+ .enum(['text', 'number', 'checkbox', 'date', 'list', 'clear'])
1686
+ .describe('The custom field type. Use "clear" to remove the value from the field.'),
1687
+ value: z
1688
+ .string()
1689
+ .optional()
1690
+ .describe(
1691
+ 'The value to set. For text: any string. For number: numeric string (e.g. "42.5"). ' +
1692
+ 'For checkbox: "true" or "false". For date: ISO 8601 (e.g. "2025-12-31T00:00:00.000Z"). ' +
1693
+ 'For list: the option ID. Not needed when type is "clear".'
1694
+ ),
1695
+ },
1696
+ },
1697
+ async ({ cardId, customFieldId, type, value }) => {
1698
+ try {
1699
+ if (type !== 'clear' && !value) {
1700
+ return {
1701
+ content: [
1702
+ {
1703
+ type: 'text' as const,
1704
+ text: 'Error: value is required when type is not "clear"',
1705
+ },
1706
+ ],
1707
+ isError: true,
1708
+ };
1709
+ }
1710
+
1711
+ const result = await this.trelloClient.updateCardCustomField(cardId, customFieldId, {
1712
+ type,
1713
+ value,
1714
+ });
1715
+ return {
1716
+ content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
1717
+ };
1718
+ } catch (error) {
1719
+ return this.handleError(error);
1720
+ }
1721
+ }
1722
+ );
1723
+
1724
+ // Card history tool
1725
+ this.server.registerTool(
1726
+ 'get_card_history',
1727
+ {
1728
+ title: 'Get Card History',
1729
+ description: 'Get the history/actions of a specific card',
1730
+ inputSchema: {
1731
+ cardId: z.string().describe('ID of the card to get history for'),
1732
+ filter: z
1733
+ .string()
1734
+ .optional()
1735
+ .describe(
1736
+ 'Optional: Filter actions by type (e.g., "all", "updateCard:idList", "addAttachmentToCard", "commentCard", "updateCard:name", "updateCard:desc", "updateCard:due", "addMemberToCard", "removeMemberFromCard", "addLabelToCard", "removeLabelFromCard")'
1737
+ ),
1738
+ limit: z
1739
+ .number()
1740
+ .optional()
1741
+ .describe('Optional: Number of actions to fetch (default: all)'),
1742
+ },
1743
+ },
1744
+ async ({ cardId, filter, limit }) => {
1745
+ try {
1746
+ const history = await this.trelloClient.getCardHistory(cardId, filter, limit);
1747
+ return {
1748
+ content: [{ type: 'text' as const, text: JSON.stringify(history, null, 2) }],
1749
+ };
1750
+ } catch (error) {
1751
+ return this.handleError(error);
1752
+ }
1753
+ }
1754
+ );
1755
+
1756
+ // Download attachment tool
1757
+ this.server.registerTool(
1758
+ 'download_attachment',
1759
+ {
1760
+ title: 'Download Attachment',
1761
+ description: 'Download an attachment from a card. Returns base64-encoded data that can be saved or viewed.',
1762
+ inputSchema: {
1763
+ cardId: z.string().describe('ID of the card containing the attachment'),
1764
+ attachmentId: z.string().describe('ID of the attachment to download'),
1765
+ },
1766
+ },
1767
+ async ({ cardId, attachmentId }) => {
1768
+ try {
1769
+ const result = await this.trelloClient.downloadAttachment(cardId, attachmentId);
1770
+
1771
+ if (result.mimeType.startsWith('image/')) {
1772
+ return {
1773
+ content: [
1774
+ {
1775
+ type: 'image' as const,
1776
+ data: result.data,
1777
+ mimeType: result.mimeType,
1778
+ },
1779
+ {
1780
+ type: 'text' as const,
1781
+ text: `Downloaded: ${result.fileName} (${result.mimeType})`,
1782
+ },
1783
+ ],
1784
+ };
1785
+ }
1786
+
1787
+ return {
1788
+ content: [
1789
+ {
1790
+ type: 'text' as const,
1791
+ text: JSON.stringify(
1792
+ { fileName: result.fileName, mimeType: result.mimeType, data: result.data },
1793
+ null,
1794
+ 2
1795
+ ),
1796
+ },
1797
+ ],
1798
+ };
1799
+ } catch (error) {
1800
+ return this.handleError(error);
1801
+ }
1802
+ }
1803
+ );
1804
+ }
1805
+
1806
+ private setupHealthEndpoints() {
1807
+ // Basic health check endpoint
1808
+ this.server.registerTool('get_health', HealthEndpointSchemas.basicHealth, async () => {
1809
+ try {
1810
+ return await this.healthEndpoints.getBasicHealth();
1811
+ } catch (error) {
1812
+ return this.handleError(error);
1813
+ }
1814
+ });
1815
+
1816
+ // Detailed health diagnostic endpoint
1817
+ this.server.registerTool(
1818
+ 'get_health_detailed',
1819
+ HealthEndpointSchemas.detailedHealth,
1820
+ async () => {
1821
+ try {
1822
+ return await this.healthEndpoints.getDetailedHealth();
1823
+ } catch (error) {
1824
+ return this.handleError(error);
1825
+ }
1826
+ }
1827
+ );
1828
+
1829
+ // Metadata consistency check endpoint
1830
+ this.server.registerTool(
1831
+ 'get_health_metadata',
1832
+ HealthEndpointSchemas.metadataHealth,
1833
+ async () => {
1834
+ try {
1835
+ return await this.healthEndpoints.getMetadataHealth();
1836
+ } catch (error) {
1837
+ return this.handleError(error);
1838
+ }
1839
+ }
1840
+ );
1841
+
1842
+ // Performance metrics endpoint
1843
+ this.server.registerTool(
1844
+ 'get_health_performance',
1845
+ HealthEndpointSchemas.performanceHealth,
1846
+ async () => {
1847
+ try {
1848
+ return await this.healthEndpoints.getPerformanceHealth();
1849
+ } catch (error) {
1850
+ return this.handleError(error);
1851
+ }
1852
+ }
1853
+ );
1854
+
1855
+ // System repair endpoint
1856
+ this.server.registerTool('perform_system_repair', HealthEndpointSchemas.repair, async () => {
1857
+ try {
1858
+ return await this.healthEndpoints.performRepair();
1859
+ } catch (error) {
1860
+ return this.handleError(error);
1861
+ }
1862
+ });
1863
+ }
1864
+
1865
+ async run() {
1866
+ const transport = new StdioServerTransport();
1867
+ // Load configuration before starting the server
1868
+ await this.trelloClient.loadConfig().catch(() => {
1869
+ // Continue with default config if loading fails
1870
+ });
1871
+ await this.server.connect(transport);
1872
+ }
1873
+ }
1874
+
1875
+ const server = new TrelloServer();
1876
+ server.run().catch(() => {
1877
+ // Silently handle errors to avoid interfering with MCP protocol
1878
+ });