trelly 0.1.1 → 0.2.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.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: trelly-mcp
3
+ description: >-
4
+ Configure and use the trelly MCP stdio server (trello_boards_list,
5
+ trello_card_create, trello_search, etc.). Use when wiring Cursor/Claude MCP,
6
+ calling Trello from an IDE agent, or choosing between MCP tools vs trelly CLI.
7
+ ---
8
+
9
+ # trelly-mcp
10
+
11
+ MCP server for Trello (npm package **trelly**, bin **`trelly-mcp`**). Returns JSON
12
+ envelope on every tool: `{ ok, profile, data }` /
13
+ `{ ok: false, error, status?, details? }`. Never uses CLI human/Ink output.
14
+
15
+ ## Setup
16
+
17
+ ### 1. Auth (CLI, once)
18
+
19
+ ```bash
20
+ trelly auth setup
21
+ trelly auth login
22
+ trelly auth list
23
+ ```
24
+
25
+ ### 2. Cursor — plugin or `~/.cursor/mcp.json`
26
+
27
+ **Plugin (skills + MCP):** after `npm install -g trelly`:
28
+
29
+ ```bash
30
+ cp -R "$(npm root -g)/trelly" ~/.cursor/plugins/local/trelly
31
+ # or from clone: ./bin/install-cursor-plugin-local.sh
32
+ ```
33
+
34
+ **MCP only** — `~/.cursor/mcp.json`:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "trelly": {
40
+ "command": "trelly-mcp",
41
+ "env": {
42
+ "TRELLO_PROFILE": "default"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ See [mcp.example.json](../../mcp.example.json).
50
+
51
+ ### 3. Restart the IDE
52
+
53
+ Reload MCP servers after editing config.
54
+
55
+ ### 4. Smoke test (optional)
56
+
57
+ ```bash
58
+ cd trello-cli && bun run mcp
59
+ ```
60
+
61
+ Starts stdio MCP manually (IDE normally launches `trelly-mcp` itself).
62
+
63
+ ## Using tools
64
+
65
+ - Pass **`profile`** on any tool for a non-default account (or set `TRELLO_PROFILE` in env).
66
+ - Read **`ok`** before using **`data`**.
67
+ - Prefer **`trello_card_archive`** / **`trello_board_archive`** to close items.
68
+ - **`trello_card_delete`** is **permanent** — no MCP board-delete tool.
69
+
70
+ ## Tool catalog
71
+
72
+ | Tool | Purpose |
73
+ |------|---------|
74
+ | `trello_profiles_list` | Saved profiles + default |
75
+ | `trello_member_me` | Authenticated member |
76
+ | `trello_boards_list` | Member boards (`filter`, `fields`) |
77
+ | `trello_board_get` | Board by id |
78
+ | `trello_board_create` | Create board |
79
+ | `trello_board_archive` | Close board (reversible) |
80
+ | `trello_board_lists` | Lists on board |
81
+ | `trello_board_cards` | All cards on board |
82
+ | `trello_list_create` | Create list |
83
+ | `trello_list_cards` | Cards in list |
84
+ | `trello_card_get` | Card (`fields` optional) |
85
+ | `trello_card_create` | Create card on list |
86
+ | `trello_card_update` | Update card fields map |
87
+ | `trello_card_move` | Move to another list |
88
+ | `trello_card_comments` | List comments |
89
+ | `trello_card_comment` | Add comment |
90
+ | `trello_card_archive` | Close card (reversible) |
91
+ | `trello_card_delete` | **Permanent** delete |
92
+ | `trello_checklist_create` | Checklist on card |
93
+ | `trello_checklist_add_item` | Checklist item |
94
+ | `trello_label_create` | Board label |
95
+ | `trello_card_add_label` | Label on card |
96
+ | `trello_search` | Search Trello |
97
+ | `trello_webhooks_list` | Token webhooks |
98
+ | `trello_webhook_create` | Create webhook |
99
+ | `trello_webhook_delete` | Delete webhook |
100
+ | `trello_api` | Raw REST (`method`, `path`, `query`, `body`) |
101
+
102
+ ## When to use MCP vs CLI
103
+
104
+ | Use MCP | Use CLI |
105
+ |---------|---------|
106
+ | IDE agent managing Trello in chat | Terminal, shell scripts, jq pipelines |
107
+ | Structured tool calls with schemas | `trello ui` interactive board |
108
+ | Cursor/Codex/Claude with MCP wired | CI with `--json` |
109
+
110
+ For terminal work, invoke the **trelly** skill instead.
111
+
112
+ ## Extending
113
+
114
+ New tools: `src/mcp/tools/` — zod input, envelope `outputSchema`,
115
+ `readOnlyHint` / `destructiveHint`. See [AGENTS.md](../../AGENTS.md).
package/src/api/client.ts CHANGED
@@ -28,7 +28,7 @@ export class TrelloClient {
28
28
  method: string,
29
29
  path: string,
30
30
  query: Query = {},
31
- body?: JsonValue,
31
+ body?: JsonValue | FormData,
32
32
  ): Promise<T> {
33
33
  const url = new URL(`${this.base}${path.startsWith("/") ? path : `/${path}`}`);
34
34
 
@@ -43,7 +43,9 @@ export class TrelloClient {
43
43
  };
44
44
 
45
45
  const init: RequestInit = { method, headers };
46
- if (body !== undefined) {
46
+ if (body instanceof FormData) {
47
+ init.body = body; // fetch sets the multipart boundary
48
+ } else if (body !== undefined) {
47
49
  headers["Content-Type"] = "application/json";
48
50
  init.body = JSON.stringify(body);
49
51
  }
@@ -63,7 +65,11 @@ export class TrelloClient {
63
65
  return this.request("GET", path, query);
64
66
  }
65
67
 
66
- post<T = JsonValue>(path: string, query?: Query, body?: JsonValue): Promise<T> {
68
+ post<T = JsonValue>(
69
+ path: string,
70
+ query?: Query,
71
+ body?: JsonValue | FormData,
72
+ ): Promise<T> {
67
73
  return this.request("POST", path, query, body);
68
74
  }
69
75
 
@@ -218,6 +224,15 @@ export class TrelloClient {
218
224
  return this.post(`/cards/${id}/attachments`, query);
219
225
  }
220
226
 
227
+ /** Upload a local file as a card attachment (multipart). */
228
+ cardUploadAttachment(id: string, form: FormData) {
229
+ return this.post(`/cards/${id}/attachments`, {}, form);
230
+ }
231
+
232
+ cardDeleteAttachment(id: string, attachmentId: string) {
233
+ return this.delete(`/cards/${id}/attachments/${attachmentId}`);
234
+ }
235
+
221
236
  cardCustomFieldItems(id: string) {
222
237
  return this.get(`/cards/${id}/customFieldItems`);
223
238
  }
@@ -71,7 +71,7 @@ export function registerBoardCommands(program: Command): void {
71
71
  boards
72
72
  .command("archive <id>")
73
73
  .description("Close (archive) a board — reversible in Trello UI")
74
- .action((id, cmd) =>
74
+ .action((id, _opts, cmd) =>
75
75
  run(cmd, async () => {
76
76
  const { client } = getClient(rootOpts(cmd).profile);
77
77
  return client.boardArchive(id);
@@ -81,7 +81,7 @@ export function registerBoardCommands(program: Command): void {
81
81
  boards
82
82
  .command("delete <id>")
83
83
  .description("Permanently delete a board — irreversible")
84
- .action((id, cmd) =>
84
+ .action((id, _opts, cmd) =>
85
85
  run(cmd, async () => {
86
86
  const { client } = getClient(rootOpts(cmd).profile);
87
87
  return client.boardDelete(id);
@@ -102,7 +102,7 @@ export function registerBoardCommands(program: Command): void {
102
102
  boards
103
103
  .command("cards <boardId>")
104
104
  .description("List cards on a board")
105
- .action((boardId, cmd) =>
105
+ .action((boardId, _opts, cmd) =>
106
106
  run(cmd, async () => {
107
107
  const { client } = getClient(rootOpts(cmd).profile);
108
108
  return client.boardCards(boardId);
@@ -112,7 +112,7 @@ export function registerBoardCommands(program: Command): void {
112
112
  boards
113
113
  .command("labels <boardId>")
114
114
  .description("List labels on a board")
115
- .action((boardId, cmd) =>
115
+ .action((boardId, _opts, cmd) =>
116
116
  run(cmd, async () => {
117
117
  const { client } = getClient(rootOpts(cmd).profile);
118
118
  return client.boardLabels(boardId);
@@ -122,7 +122,7 @@ export function registerBoardCommands(program: Command): void {
122
122
  boards
123
123
  .command("members <boardId>")
124
124
  .description("List board members")
125
- .action((boardId, cmd) =>
125
+ .action((boardId, _opts, cmd) =>
126
126
  run(cmd, async () => {
127
127
  const { client } = getClient(rootOpts(cmd).profile);
128
128
  return client.boardMembers(boardId);
@@ -143,7 +143,7 @@ export function registerBoardCommands(program: Command): void {
143
143
  boards
144
144
  .command("custom-fields <boardId>")
145
145
  .description("List custom field definitions")
146
- .action((boardId, cmd) =>
146
+ .action((boardId, _opts, cmd) =>
147
147
  run(cmd, async () => {
148
148
  const { client } = getClient(rootOpts(cmd).profile);
149
149
  return client.boardCustomFields(boardId);
@@ -1,4 +1,5 @@
1
1
  import type { Command } from "commander";
2
+ import { attachmentForm } from "../../util/attachment.ts";
2
3
  import { getClient, parseJsonFlag, parseKvPairs } from "../context.ts";
3
4
  import { rootOpts, run } from "./run.ts";
4
5
 
@@ -176,15 +177,32 @@ export function registerCardCommands(program: Command): void {
176
177
 
177
178
  cards
178
179
  .command("add-attachment <id>")
179
- .requiredOption("--url <url>", "Attachment URL")
180
+ .option("--url <url>", "Attachment URL")
181
+ .option("--file <path>", "Local file to upload")
180
182
  .option("--name <name>", "Attachment name")
181
183
  .action((id, opts, cmd) =>
182
184
  run(cmd, async () => {
183
185
  const { client } = getClient(rootOpts(cmd).profile);
186
+ if (!opts.url === !opts.file) {
187
+ throw new Error("Pass exactly one of --url or --file");
188
+ }
189
+ if (opts.file) {
190
+ return client.cardUploadAttachment(id, attachmentForm(opts.file, opts.name));
191
+ }
184
192
  return client.cardAddAttachment(id, { url: opts.url, name: opts.name });
185
193
  }),
186
194
  );
187
195
 
196
+ cards
197
+ .command("delete-attachment <id> <attachmentId>")
198
+ .description("Delete an attachment from a card")
199
+ .action((id, attachmentId, _opts, cmd) =>
200
+ run(cmd, async () => {
201
+ const { client } = getClient(rootOpts(cmd).profile);
202
+ return client.cardDeleteAttachment(id, attachmentId);
203
+ }),
204
+ );
205
+
188
206
  cards.command("custom-fields <id>").action((id, _opts, cmd) =>
189
207
  run(cmd, async () => {
190
208
  const { client } = getClient(rootOpts(cmd).profile);