@delorenj/mcp-server-trello 0.1.1
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/LICENSE +21 -0
- package/README.md +252 -0
- package/build/index.js +308 -0
- package/build/rate-limiter.js +55 -0
- package/build/trello-client.js +118 -0
- package/build/types.js +1 -0
- package/build/validators.js +104 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Model Context Protocol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# MCP Server Trello
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that provides tools for interacting with Trello boards. This server enables seamless integration with Trello's API while handling rate limiting, type safety, and error handling automatically.
|
|
4
|
+
|
|
5
|
+
## Changelog
|
|
6
|
+
|
|
7
|
+
### 0.1.1
|
|
8
|
+
|
|
9
|
+
- Added `move_card` tool to move cards between lists
|
|
10
|
+
- Improved documentation
|
|
11
|
+
|
|
12
|
+
### 0.1.0
|
|
13
|
+
|
|
14
|
+
- Initial release with basic Trello board management features
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Full Trello Board Integration**: Interact with cards, lists, and board activities
|
|
19
|
+
- **Built-in Rate Limiting**: Respects Trello's API limits (300 requests/10s per API key, 100 requests/10s per token)
|
|
20
|
+
- **Type-Safe Implementation**: Written in TypeScript with comprehensive type definitions
|
|
21
|
+
- **Input Validation**: Robust validation for all API inputs
|
|
22
|
+
- **Error Handling**: Graceful error handling with informative messages
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @delorenj/mcp-server-trello
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Add the server to your MCP settings file with the following configuration:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"trello": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-y", "@delorenj/mcp-server-trello"],
|
|
40
|
+
"env": {
|
|
41
|
+
"TRELLO_API_KEY": "your-api-key",
|
|
42
|
+
"TRELLO_TOKEN": "your-token",
|
|
43
|
+
"TRELLO_BOARD_ID": "your-board-id"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Required Environment Variables
|
|
51
|
+
|
|
52
|
+
- `TRELLO_API_KEY`: Your Trello API key (get from <https://trello.com/app-key>)
|
|
53
|
+
- `TRELLO_TOKEN`: Your Trello token (generate using your API key)
|
|
54
|
+
- `TRELLO_BOARD_ID`: ID of the Trello board to interact with (found in board URL)
|
|
55
|
+
|
|
56
|
+
## Available Tools
|
|
57
|
+
|
|
58
|
+
### get_cards_by_list_id
|
|
59
|
+
|
|
60
|
+
Fetch all cards from a specific list.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
{
|
|
64
|
+
name: 'get_cards_by_list_id',
|
|
65
|
+
arguments: {
|
|
66
|
+
listId: string // ID of the Trello list
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### get_lists
|
|
72
|
+
|
|
73
|
+
Retrieve all lists from the configured board.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
{
|
|
77
|
+
name: 'get_lists',
|
|
78
|
+
arguments: {}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### get_recent_activity
|
|
83
|
+
|
|
84
|
+
Fetch recent activity on the board.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
{
|
|
88
|
+
name: 'get_recent_activity',
|
|
89
|
+
arguments: {
|
|
90
|
+
limit?: number // Optional: Number of activities to fetch (default: 10)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### add_card_to_list
|
|
96
|
+
|
|
97
|
+
Add a new card to a specified list.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
{
|
|
101
|
+
name: 'add_card_to_list',
|
|
102
|
+
arguments: {
|
|
103
|
+
listId: string, // ID of the list to add the card to
|
|
104
|
+
name: string, // Name of the card
|
|
105
|
+
description?: string, // Optional: Description of the card
|
|
106
|
+
dueDate?: string, // Optional: Due date (ISO 8601 format)
|
|
107
|
+
labels?: string[] // Optional: Array of label IDs
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### update_card_details
|
|
113
|
+
|
|
114
|
+
Update an existing card's details.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
{
|
|
118
|
+
name: 'update_card_details',
|
|
119
|
+
arguments: {
|
|
120
|
+
cardId: string, // ID of the card to update
|
|
121
|
+
name?: string, // Optional: New name for the card
|
|
122
|
+
description?: string, // Optional: New description
|
|
123
|
+
dueDate?: string, // Optional: New due date (ISO 8601 format)
|
|
124
|
+
labels?: string[] // Optional: New array of label IDs
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### archive_card
|
|
130
|
+
|
|
131
|
+
Send a card to the archive.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
{
|
|
135
|
+
name: 'archive_card',
|
|
136
|
+
arguments: {
|
|
137
|
+
cardId: string // ID of the card to archive
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### add_list_to_board
|
|
143
|
+
|
|
144
|
+
Add a new list to the board.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
{
|
|
148
|
+
name: 'add_list_to_board',
|
|
149
|
+
arguments: {
|
|
150
|
+
name: string // Name of the new list
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### archive_list
|
|
156
|
+
|
|
157
|
+
Send a list to the archive.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
{
|
|
161
|
+
name: 'archive_list',
|
|
162
|
+
arguments: {
|
|
163
|
+
listId: string // ID of the list to archive
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### get_my_cards
|
|
169
|
+
|
|
170
|
+
Fetch all cards assigned to the current user.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
{
|
|
174
|
+
name: 'get_my_cards',
|
|
175
|
+
arguments: {}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### move_card
|
|
180
|
+
|
|
181
|
+
Move a card to a different list.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
{
|
|
185
|
+
name: 'move_card',
|
|
186
|
+
arguments: {
|
|
187
|
+
cardId: string, // ID of the card to move
|
|
188
|
+
listId: string // ID of the target list
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Rate Limiting
|
|
194
|
+
|
|
195
|
+
The server implements a token bucket algorithm for rate limiting to comply with Trello's API limits:
|
|
196
|
+
|
|
197
|
+
- 300 requests per 10 seconds per API key
|
|
198
|
+
- 100 requests per 10 seconds per token
|
|
199
|
+
|
|
200
|
+
Rate limiting is handled automatically, and requests will be queued if limits are reached.
|
|
201
|
+
|
|
202
|
+
## Error Handling
|
|
203
|
+
|
|
204
|
+
The server provides detailed error messages for various scenarios:
|
|
205
|
+
|
|
206
|
+
- Invalid input parameters
|
|
207
|
+
- Rate limit exceeded
|
|
208
|
+
- API authentication errors
|
|
209
|
+
- Network issues
|
|
210
|
+
- Invalid board/list/card IDs
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
### Prerequisites
|
|
215
|
+
|
|
216
|
+
- Node.js 16 or higher
|
|
217
|
+
- npm or yarn
|
|
218
|
+
|
|
219
|
+
### Setup
|
|
220
|
+
|
|
221
|
+
1. Clone the repository
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
git clone https://github.com/delorenj/mcp-server-trello
|
|
225
|
+
cd mcp-server-trello
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
2. Install dependencies
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npm install
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
3. Build the project
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npm run build
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
Contributions are welcome!
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
248
|
+
|
|
249
|
+
## Acknowledgments
|
|
250
|
+
|
|
251
|
+
- Built with the [Model Context Protocol SDK](https://github.com/modelcontextprotocol/sdk)
|
|
252
|
+
- Uses the [Trello REST API](https://developer.atlassian.com/cloud/trello/rest/)
|
package/build/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { TrelloClient } from './trello-client.js';
|
|
6
|
+
import { validateGetCardsListRequest, validateGetRecentActivityRequest, validateAddCardRequest, validateUpdateCardRequest, validateArchiveCardRequest, validateAddListRequest, validateArchiveListRequest, validateMoveCardRequest, } from './validators.js';
|
|
7
|
+
class TrelloServer {
|
|
8
|
+
constructor() {
|
|
9
|
+
const apiKey = process.env.TRELLO_API_KEY;
|
|
10
|
+
const token = process.env.TRELLO_TOKEN;
|
|
11
|
+
const boardId = process.env.TRELLO_BOARD_ID;
|
|
12
|
+
if (!apiKey || !token || !boardId) {
|
|
13
|
+
throw new Error('TRELLO_API_KEY, TRELLO_TOKEN, and TRELLO_BOARD_ID environment variables are required');
|
|
14
|
+
}
|
|
15
|
+
this.trelloClient = new TrelloClient({ apiKey, token, boardId });
|
|
16
|
+
this.server = new Server({
|
|
17
|
+
name: 'trello-server',
|
|
18
|
+
version: '0.1.1',
|
|
19
|
+
}, {
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools: {},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
this.setupToolHandlers();
|
|
25
|
+
// Error handling
|
|
26
|
+
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
27
|
+
process.on('SIGINT', async () => {
|
|
28
|
+
await this.server.close();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
setupToolHandlers() {
|
|
33
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
34
|
+
tools: [
|
|
35
|
+
{
|
|
36
|
+
name: 'get_cards_by_list_id',
|
|
37
|
+
description: 'Fetch cards from a specific Trello list',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
listId: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'ID of the Trello list',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['listId'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'get_lists',
|
|
51
|
+
description: 'Retrieve all lists from the specified board',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {},
|
|
55
|
+
required: [],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'get_recent_activity',
|
|
60
|
+
description: 'Fetch recent activity on the Trello board',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
limit: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
description: 'Number of activities to fetch (default: 10)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: [],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'add_card_to_list',
|
|
74
|
+
description: 'Add a new card to a specified list',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
listId: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'ID of the list to add the card to',
|
|
81
|
+
},
|
|
82
|
+
name: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Name of the card',
|
|
85
|
+
},
|
|
86
|
+
description: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Description of the card',
|
|
89
|
+
},
|
|
90
|
+
dueDate: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Due date for the card (ISO 8601 format)',
|
|
93
|
+
},
|
|
94
|
+
labels: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
items: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
},
|
|
99
|
+
description: 'Array of label IDs to apply to the card',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ['listId', 'name'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'update_card_details',
|
|
107
|
+
description: 'Update an existing card\'s details',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
cardId: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'ID of the card to update',
|
|
114
|
+
},
|
|
115
|
+
name: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'New name for the card',
|
|
118
|
+
},
|
|
119
|
+
description: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'New description for the card',
|
|
122
|
+
},
|
|
123
|
+
dueDate: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'New due date for the card (ISO 8601 format)',
|
|
126
|
+
},
|
|
127
|
+
labels: {
|
|
128
|
+
type: 'array',
|
|
129
|
+
items: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
},
|
|
132
|
+
description: 'New array of label IDs for the card',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ['cardId'],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'archive_card',
|
|
140
|
+
description: 'Send a card to the archive',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
cardId: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'ID of the card to archive',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
required: ['cardId'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'move_card',
|
|
154
|
+
description: 'Move a card to a different list',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
cardId: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'ID of the card to move',
|
|
161
|
+
},
|
|
162
|
+
listId: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'ID of the target list',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
required: ['cardId', 'listId'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'add_list_to_board',
|
|
172
|
+
description: 'Add a new list to the board',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
name: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
description: 'Name of the new list',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ['name'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'archive_list',
|
|
186
|
+
description: 'Send a list to the archive',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
listId: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'ID of the list to archive',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ['listId'],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'get_my_cards',
|
|
200
|
+
description: 'Fetch all cards assigned to the current user',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {},
|
|
204
|
+
required: [],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}));
|
|
209
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
210
|
+
try {
|
|
211
|
+
if (!request.params.arguments) {
|
|
212
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing arguments');
|
|
213
|
+
}
|
|
214
|
+
const args = request.params.arguments;
|
|
215
|
+
switch (request.params.name) {
|
|
216
|
+
case 'get_cards_by_list_id': {
|
|
217
|
+
const validArgs = validateGetCardsListRequest(args);
|
|
218
|
+
const cards = await this.trelloClient.getCardsByList(validArgs.listId);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text', text: JSON.stringify(cards, null, 2) }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
case 'get_lists': {
|
|
224
|
+
const lists = await this.trelloClient.getLists();
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: 'text', text: JSON.stringify(lists, null, 2) }],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
case 'get_recent_activity': {
|
|
230
|
+
const validArgs = validateGetRecentActivityRequest(args);
|
|
231
|
+
const activity = await this.trelloClient.getRecentActivity(validArgs.limit);
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: 'text', text: JSON.stringify(activity, null, 2) }],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
case 'add_card_to_list': {
|
|
237
|
+
const validArgs = validateAddCardRequest(args);
|
|
238
|
+
const card = await this.trelloClient.addCard(validArgs);
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: 'text', text: JSON.stringify(card, null, 2) }],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
case 'update_card_details': {
|
|
244
|
+
const validArgs = validateUpdateCardRequest(args);
|
|
245
|
+
const card = await this.trelloClient.updateCard(validArgs);
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: 'text', text: JSON.stringify(card, null, 2) }],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
case 'archive_card': {
|
|
251
|
+
const validArgs = validateArchiveCardRequest(args);
|
|
252
|
+
const card = await this.trelloClient.archiveCard(validArgs.cardId);
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: 'text', text: JSON.stringify(card, null, 2) }],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
case 'move_card': {
|
|
258
|
+
const validArgs = validateMoveCardRequest(args);
|
|
259
|
+
const card = await this.trelloClient.moveCard(validArgs.cardId, validArgs.listId);
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: 'text', text: JSON.stringify(card, null, 2) }],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
case 'add_list_to_board': {
|
|
265
|
+
const validArgs = validateAddListRequest(args);
|
|
266
|
+
const list = await this.trelloClient.addList(validArgs.name);
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: 'text', text: JSON.stringify(list, null, 2) }],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
case 'archive_list': {
|
|
272
|
+
const validArgs = validateArchiveListRequest(args);
|
|
273
|
+
const list = await this.trelloClient.archiveList(validArgs.listId);
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: 'text', text: JSON.stringify(list, null, 2) }],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
case 'get_my_cards': {
|
|
279
|
+
const cards = await this.trelloClient.getMyCards();
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: 'text', text: JSON.stringify(cards, null, 2) }],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: 'text',
|
|
293
|
+
text: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
isError: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async run() {
|
|
302
|
+
const transport = new StdioServerTransport();
|
|
303
|
+
await this.server.connect(transport);
|
|
304
|
+
console.error('Trello MCP server running on stdio');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const server = new TrelloServer();
|
|
308
|
+
server.run().catch(console.error);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class TokenBucketRateLimiter {
|
|
2
|
+
constructor(maxRequests, windowMs) {
|
|
3
|
+
this.maxTokens = maxRequests;
|
|
4
|
+
this.tokens = maxRequests;
|
|
5
|
+
this.lastRefill = Date.now();
|
|
6
|
+
this.refillInterval = windowMs;
|
|
7
|
+
this.refillRate = maxRequests / windowMs;
|
|
8
|
+
}
|
|
9
|
+
refillTokens() {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const timePassed = now - this.lastRefill;
|
|
12
|
+
const newTokens = timePassed * this.refillRate;
|
|
13
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
14
|
+
this.lastRefill = now;
|
|
15
|
+
}
|
|
16
|
+
canMakeRequest() {
|
|
17
|
+
this.refillTokens();
|
|
18
|
+
if (this.tokens >= 1) {
|
|
19
|
+
this.tokens -= 1;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
async waitForAvailableToken() {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const check = () => {
|
|
27
|
+
if (this.canMakeRequest()) {
|
|
28
|
+
resolve();
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Calculate time until next token is available
|
|
32
|
+
const tokensNeeded = 1 - this.tokens;
|
|
33
|
+
const msToWait = (tokensNeeded / this.refillRate) * 1000;
|
|
34
|
+
setTimeout(check, Math.min(msToWait, 100)); // Check at most every 100ms
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
check();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Create rate limiters based on Trello's limits
|
|
42
|
+
export const createTrelloRateLimiters = () => {
|
|
43
|
+
const apiKeyLimiter = new TokenBucketRateLimiter(300, 10000); // 300 requests per 10 seconds
|
|
44
|
+
const tokenLimiter = new TokenBucketRateLimiter(100, 10000); // 100 requests per 10 seconds
|
|
45
|
+
return {
|
|
46
|
+
apiKeyLimiter,
|
|
47
|
+
tokenLimiter,
|
|
48
|
+
async waitForAvailable() {
|
|
49
|
+
await Promise.all([
|
|
50
|
+
apiKeyLimiter.waitForAvailableToken(),
|
|
51
|
+
tokenLimiter.waitForAvailableToken(),
|
|
52
|
+
]);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createTrelloRateLimiters } from './rate-limiter.js';
|
|
3
|
+
export class TrelloClient {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.axiosInstance = axios.create({
|
|
7
|
+
baseURL: 'https://api.trello.com/1',
|
|
8
|
+
params: {
|
|
9
|
+
key: config.apiKey,
|
|
10
|
+
token: config.token,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
this.rateLimiter = createTrelloRateLimiters();
|
|
14
|
+
// Add rate limiting interceptor
|
|
15
|
+
this.axiosInstance.interceptors.request.use(async (config) => {
|
|
16
|
+
await this.rateLimiter.waitForAvailable();
|
|
17
|
+
return config;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async handleRequest(request) {
|
|
21
|
+
try {
|
|
22
|
+
return await request();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (axios.isAxiosError(error)) {
|
|
26
|
+
if (error.response?.status === 429) {
|
|
27
|
+
// Rate limit exceeded, wait and retry
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
29
|
+
return this.handleRequest(request);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Trello API error: ${error.response?.data?.message ?? error.message}`);
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async getCardsByList(listId) {
|
|
37
|
+
return this.handleRequest(async () => {
|
|
38
|
+
const response = await this.axiosInstance.get(`/lists/${listId}/cards`);
|
|
39
|
+
return response.data;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async getLists() {
|
|
43
|
+
return this.handleRequest(async () => {
|
|
44
|
+
const response = await this.axiosInstance.get(`/boards/${this.config.boardId}/lists`);
|
|
45
|
+
return response.data;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async getRecentActivity(limit = 10) {
|
|
49
|
+
return this.handleRequest(async () => {
|
|
50
|
+
const response = await this.axiosInstance.get(`/boards/${this.config.boardId}/actions`, {
|
|
51
|
+
params: { limit },
|
|
52
|
+
});
|
|
53
|
+
return response.data;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async addCard(params) {
|
|
57
|
+
return this.handleRequest(async () => {
|
|
58
|
+
const response = await this.axiosInstance.post('/cards', {
|
|
59
|
+
idList: params.listId,
|
|
60
|
+
name: params.name,
|
|
61
|
+
desc: params.description,
|
|
62
|
+
due: params.dueDate,
|
|
63
|
+
idLabels: params.labels,
|
|
64
|
+
});
|
|
65
|
+
return response.data;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async updateCard(params) {
|
|
69
|
+
return this.handleRequest(async () => {
|
|
70
|
+
const response = await this.axiosInstance.put(`/cards/${params.cardId}`, {
|
|
71
|
+
name: params.name,
|
|
72
|
+
desc: params.description,
|
|
73
|
+
due: params.dueDate,
|
|
74
|
+
idLabels: params.labels,
|
|
75
|
+
});
|
|
76
|
+
return response.data;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async archiveCard(cardId) {
|
|
80
|
+
return this.handleRequest(async () => {
|
|
81
|
+
const response = await this.axiosInstance.put(`/cards/${cardId}`, {
|
|
82
|
+
closed: true,
|
|
83
|
+
});
|
|
84
|
+
return response.data;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async moveCard(cardId, listId) {
|
|
88
|
+
return this.handleRequest(async () => {
|
|
89
|
+
const response = await this.axiosInstance.put(`/cards/${cardId}`, {
|
|
90
|
+
idList: listId,
|
|
91
|
+
});
|
|
92
|
+
return response.data;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async addList(name) {
|
|
96
|
+
return this.handleRequest(async () => {
|
|
97
|
+
const response = await this.axiosInstance.post('/lists', {
|
|
98
|
+
name,
|
|
99
|
+
idBoard: this.config.boardId,
|
|
100
|
+
});
|
|
101
|
+
return response.data;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async archiveList(listId) {
|
|
105
|
+
return this.handleRequest(async () => {
|
|
106
|
+
const response = await this.axiosInstance.put(`/lists/${listId}/closed`, {
|
|
107
|
+
value: true,
|
|
108
|
+
});
|
|
109
|
+
return response.data;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async getMyCards() {
|
|
113
|
+
return this.handleRequest(async () => {
|
|
114
|
+
const response = await this.axiosInstance.get('/members/me/cards');
|
|
115
|
+
return response.data;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export function validateString(value, field) {
|
|
3
|
+
if (typeof value !== 'string') {
|
|
4
|
+
throw new McpError(ErrorCode.InvalidParams, `${field} must be a string`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function validateOptionalString(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return validateString(value, 'value');
|
|
12
|
+
}
|
|
13
|
+
export function validateNumber(value, field) {
|
|
14
|
+
if (typeof value !== 'number') {
|
|
15
|
+
throw new McpError(ErrorCode.InvalidParams, `${field} must be a number`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function validateOptionalNumber(value) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return validateNumber(value, 'value');
|
|
23
|
+
}
|
|
24
|
+
export function validateStringArray(value) {
|
|
25
|
+
if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) {
|
|
26
|
+
throw new McpError(ErrorCode.InvalidParams, 'Value must be an array of strings');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function validateOptionalStringArray(value) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
return validateStringArray(value);
|
|
34
|
+
}
|
|
35
|
+
export function validateGetCardsListRequest(args) {
|
|
36
|
+
if (!args.listId) {
|
|
37
|
+
throw new McpError(ErrorCode.InvalidParams, 'listId is required');
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
listId: validateString(args.listId, 'listId'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function validateGetRecentActivityRequest(args) {
|
|
44
|
+
return {
|
|
45
|
+
limit: validateOptionalNumber(args.limit),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function validateAddCardRequest(args) {
|
|
49
|
+
if (!args.listId || !args.name) {
|
|
50
|
+
throw new McpError(ErrorCode.InvalidParams, 'listId and name are required');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
listId: validateString(args.listId, 'listId'),
|
|
54
|
+
name: validateString(args.name, 'name'),
|
|
55
|
+
description: validateOptionalString(args.description),
|
|
56
|
+
dueDate: validateOptionalString(args.dueDate),
|
|
57
|
+
labels: validateOptionalStringArray(args.labels),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function validateUpdateCardRequest(args) {
|
|
61
|
+
if (!args.cardId) {
|
|
62
|
+
throw new McpError(ErrorCode.InvalidParams, 'cardId is required');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
cardId: validateString(args.cardId, 'cardId'),
|
|
66
|
+
name: validateOptionalString(args.name),
|
|
67
|
+
description: validateOptionalString(args.description),
|
|
68
|
+
dueDate: validateOptionalString(args.dueDate),
|
|
69
|
+
labels: validateOptionalStringArray(args.labels),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function validateArchiveCardRequest(args) {
|
|
73
|
+
if (!args.cardId) {
|
|
74
|
+
throw new McpError(ErrorCode.InvalidParams, 'cardId is required');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
cardId: validateString(args.cardId, 'cardId'),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function validateAddListRequest(args) {
|
|
81
|
+
if (!args.name) {
|
|
82
|
+
throw new McpError(ErrorCode.InvalidParams, 'name is required');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name: validateString(args.name, 'name'),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function validateArchiveListRequest(args) {
|
|
89
|
+
if (!args.listId) {
|
|
90
|
+
throw new McpError(ErrorCode.InvalidParams, 'listId is required');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
listId: validateString(args.listId, 'listId'),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function validateMoveCardRequest(args) {
|
|
97
|
+
if (!args.cardId || !args.listId) {
|
|
98
|
+
throw new McpError(ErrorCode.InvalidParams, 'cardId and listId are required');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
cardId: validateString(args.cardId, 'cardId'),
|
|
102
|
+
listId: validateString(args.listId, 'listId'),
|
|
103
|
+
};
|
|
104
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delorenj/mcp-server-trello",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A Model Context Protocol (MCP) server for interacting with Trello boards",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"types": "build/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-server-trello": "./build/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && chmod +x build/index.js",
|
|
13
|
+
"start": "node build/index.js",
|
|
14
|
+
"dev": "ts-node --esm src/index.ts",
|
|
15
|
+
"lint": "eslint . --ext .ts",
|
|
16
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
17
|
+
"prepare": "npm run build",
|
|
18
|
+
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/delorenj/mcp-server-trello.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"trello",
|
|
28
|
+
"api",
|
|
29
|
+
"integration",
|
|
30
|
+
"automation",
|
|
31
|
+
"typescript",
|
|
32
|
+
"board-management",
|
|
33
|
+
"task-management",
|
|
34
|
+
"kanban"
|
|
35
|
+
],
|
|
36
|
+
"author": "Jarad DeLorenzo",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/delorenj/mcp-server-trello/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/delorenj/mcp-server-trello#readme",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
44
|
+
"axios": "^1.6.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.10.0",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
|
49
|
+
"@typescript-eslint/parser": "^6.12.0",
|
|
50
|
+
"eslint": "^8.54.0",
|
|
51
|
+
"eslint-config-prettier": "^9.0.0",
|
|
52
|
+
"eslint-plugin-prettier": "^5.0.1",
|
|
53
|
+
"prettier": "^3.1.0",
|
|
54
|
+
"ts-node": "^10.9.1",
|
|
55
|
+
"typescript": "^5.3.2"
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"build/**/*"
|
|
59
|
+
],
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=16.0.0"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|