trelly 0.2.1 → 0.3.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/README.md CHANGED
@@ -211,29 +211,20 @@ CI runs the same via `bun install --frozen-lockfile`. See `AGENTS.md` for conven
211
211
 
212
212
  ## Agent skills & plugins
213
213
 
214
- Shipped in the npm package for **end users** — skills plus IDE plugin manifests. Full
215
- plugin docs: [PLUGIN.md](PLUGIN.md) · Privacy: [PRIVACY.md](PRIVACY.md) ·
216
- [skills/README.md](skills/README.md).
214
+ The npm package ships agent skills plus plugin manifests, so Claude Code, Cursor,
215
+ and pi agents learn the trelly CLI + MCP conventions automatically.
217
216
 
218
- ```bash
219
- npm install -g trelly
220
- trelly auth setup && trelly auth login
217
+ Install trelly and log in first (`npm install -g trelly && trelly auth setup && trelly auth login`), then:
221
218
 
222
- pi install npm:trelly
223
- claude plugin install "$(npm root -g)/trelly"
224
- ./bin/install-cursor-plugin-local.sh # Cursor local test (copy, not symlink)
219
+ ```bash
220
+ claude plugin install "$(npm root -g)/trelly" # Claude Code
221
+ "$(npm root -g)/trelly/bin/install-cursor-plugin-local.sh" # Cursor (copies plugin, then reload window)
222
+ pi install npm:trelly # pi
225
223
  ```
226
224
 
227
- MCP-only (no plugin): add `trelly-mcp` to `~/.cursor/mcp.json` — [mcp.example.json](mcp.example.json).
228
-
229
- ### Marketplace submission
230
-
231
- | Platform | Submit |
232
- |----------|--------|
233
- | **Cursor** | [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish) → `https://github.com/brandonkramer/trelly` |
234
- | **Claude Code** | [clau.de/plugin-directory-submission](https://clau.de/plugin-directory-submission) |
225
+ MCP-only (no plugin): add `trelly-mcp` to `~/.cursor/mcp.json` — see [mcp.example.json](mcp.example.json).
235
226
 
236
- Open source (MIT). After Claude listing: `/plugin install trelly@claude-plugins-official`.
227
+ Details: [PLUGIN.md](PLUGIN.md) · [PRIVACY.md](PRIVACY.md) · [skills/README.md](skills/README.md)
237
228
 
238
229
  ## License
239
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trelly",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "trelly — fast Trello CLI with multi-profile auth, MCP server, and kanban TUI",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/skills/README.md CHANGED
@@ -6,8 +6,9 @@ developing this repo.
6
6
 
7
7
  | Skill | Use when |
8
8
  |-------|----------|
9
- | [trelly](trelly/SKILL.md) | Terminal / bash: `trelly boards list`, auth, attachments, GitHub PR/commit links, `--json` |
10
- | [trelly-mcp](trelly-mcp/SKILL.md) | IDE agent with MCP wired: tool names, GitHub links via `trello_api`, envelopes, safety |
9
+ | [trelly](trelly/SKILL.md) | Terminal / bash: human card lists, auth, attachments, GitHub links, `--json` |
10
+ | [trelly-mcp](trelly-mcp/SKILL.md) | IDE + MCP: paste `display` from list tools, GitHub links, safety |
11
+ | [trelly-card-display](trelly-card-display.md) | **Required** when user asks to see/list cards (all platforms) |
11
12
 
12
13
  **One copy of the content:** `skills/trelly/` and `skills/trelly-mcp/` in the installed
13
14
  package. Plugins and Pi load from there — you don't maintain separate copies.
@@ -4,7 +4,8 @@ description: >-
4
4
  Operate the trelly Trello CLI (npm trelly; bins trelly/trello): auth setup/login,
5
5
  human vs --json output, boards/lists/cards, search, attachments, GitHub PR/commit
6
6
  links on cards, trello ui, trello api. Use when the user asks to run trelly/trello
7
- commands, link a GitHub PR or commit to a Trello card, or automate Trello with trelly.
7
+ commands, list or show Trello cards/todos, link a GitHub PR or commit to a card, or
8
+ automate Trello with trelly.
8
9
  ---
9
10
 
10
11
  # trelly
@@ -12,6 +13,10 @@ description: >-
12
13
  Fast Trello CLI (`npm install -g trelly`). **Human Trello-styled output by default**;
13
14
  **`--json` for scripts**. Commands: **`trelly`** or **`trello`** (same binary).
14
15
 
16
+ > **Showing cards to a user?** Read [trelly-card-display.md](trelly-card-display.md).
17
+ > Pi has no MCP — use **human CLI** (`trelly lists cards LIST_ID` without `--json`)
18
+ > or format per that contract. Never reply with titles-only.
19
+
15
20
  ## Prerequisites
16
21
 
17
22
  ```bash
@@ -40,6 +45,10 @@ Credentials: `~/.config/trelly/config.json` (migrates from `~/.config/trello-cli
40
45
  | Pretty JSON | `--json --pretty` |
41
46
 
42
47
  - **Scripts:** always `--json` if parsing stdout.
48
+ - **Token cost:** raw `--json` returns full Trello objects (a board list ≈ 10k tokens) — trim with `jq` (e.g. `| jq '.data[] | {id,name}'`) or request fewer fields (`--fields`, `--query "fields=id,name"`).
49
+ - **Showing cards to a human?** See [trelly-card-display.md](trelly-card-display.md).
50
+ Human CLI (`trelly lists cards LIST_ID`) already matches the contract. With `--json`,
51
+ slim first: `jq '.data[] | {name, shortUrl, labels: [.labels[] | {name, color}], badges: {comments: .badges.comments, attachments: .badges.attachments, checkItems: .badges.checkItems, checkItemsChecked: .badges.checkItemsChecked}, due, dueComplete}'`.
43
52
  - **`--pretty` alone** does not emit JSON; it only indents `--json` output.
44
53
  - Errors: red `✗` in human mode; exit code `1`.
45
54
 
@@ -0,0 +1,39 @@
1
+ # Card list display contract (all agents)
2
+
3
+ When the user asks to **see**, **list**, or **show** Trello cards (todo, backlog,
4
+ board, column, "what's on…"), you MUST NOT reply with a plain numbered title list.
5
+
6
+ ## MCP (Cursor, Claude Code, Codex with trelly-mcp)
7
+
8
+ `trello_list_cards` and `trello_board_cards` return:
9
+
10
+ - `display` — markdown-v1, ready to paste (linked titles + badges)
11
+ - `data` — slim JSON for automation
12
+
13
+ **Rule: paste `display` verbatim** (optionally add a heading via `displayHeading`).
14
+ Do not re-summarize `data` into your own format.
15
+
16
+ Example line:
17
+
18
+ ```text
19
+ 1. [Publish module behavior](https://trello.com/c/1jH2UTAu) · 💬 2 · 📎 2
20
+ ```
21
+
22
+ Badge keys (omit when zero): 💬 comments · 📎 attachments · ✓ checked/total · ⏰ due.
23
+ Labels: colored dot + `` `name` `` (🔴 🟠 🟡 🟢 🔵 🟣 ⚫ ⚪).
24
+
25
+ ## CLI / Pi (no MCP)
26
+
27
+ **Preferred:** human output (no `--json`):
28
+
29
+ ```bash
30
+ trelly lists cards LIST_ID
31
+ ```
32
+
33
+ **If using `--json`:** format with the jq recipe in `skills/trelly/SKILL.md` Output
34
+ contract, or call MCP when available.
35
+
36
+ ## Automation only
37
+
38
+ Plain `data` / names-only output is OK when the user wants scripts, counts, or IDs —
39
+ not when they asked to **see** the board.
@@ -3,7 +3,8 @@ name: trelly-mcp
3
3
  description: >-
4
4
  Configure and use the trelly MCP stdio server (trello_boards_list,
5
5
  trello_card_create, trello_search, trello_api, etc.). Use when wiring Cursor/Claude
6
- MCP, linking GitHub PRs/commits to Trello cards from an agent, or choosing MCP vs CLI.
6
+ MCP, listing or showing Trello cards/todos, linking GitHub PRs/commits, or choosing
7
+ MCP vs CLI.
7
8
  ---
8
9
 
9
10
  # trelly-mcp
@@ -12,6 +13,34 @@ MCP server for Trello (npm package **trelly**, bin **`trelly-mcp`**). Returns JS
12
13
  envelope on every tool: `{ ok, profile, data }` /
13
14
  `{ ok: false, error, status?, details? }`. Never uses CLI human/Ink output.
14
15
 
16
+ > **Listing cards for a user?** Read [trelly-card-display.md](trelly-card-display.md).
17
+ > **`trello_list_cards` / `trello_board_cards` include `display` — paste it verbatim.**
18
+
19
+ List/get tools default to lean `fields` (id, name, url, due, …) to keep responses
20
+ token-cheap; pass `fields: "all"` when you need more.
21
+ `trello_list_cards` and `trello_board_cards` include slim `badges`, `labels`, and
22
+ pre-rendered **`display`** (markdown-v1).
23
+
24
+ ## Rendering card lists for humans
25
+
26
+ **Do not reformat.** When `display` is present, show it to the user unchanged
27
+ (you may prepend context from `displayHeading` or your own board/list title).
28
+
29
+ MCP tool text also leads with `display`, then JSON — prefer the markdown block.
30
+
31
+ Manual format (only if `display` missing — e.g. raw `trello_api`):
32
+
33
+ ```
34
+ 1. [Publish module behavior](https://trello.com/c/1jH2UTAu) 🔴 `Priority: Highest` · 💬 4 · 📎 2 · ✓ 2/5 · ⏰ Jul 5
35
+ ```
36
+
37
+ - Title → `[name](shortUrl)`. Counts from `badges`: 💬 comments · 📎 attachments · ✓ checkItemsChecked/checkItems.
38
+ - Due: `⏰ Jul 5`, add `(overdue)` if past and not `dueComplete`; `✓` if complete.
39
+ - Labels: colored dot + name in backticks. Trello color → emoji: red 🔴 · orange 🟠 · yellow 🟡 · green/lime 🟢 · blue/sky 🔵 · purple/pink 🟣 · black ⚫ · none ⚪.
40
+ - Custom-field chips (e.g. `Priority: Highest`): fetch defs once via `trello_api` GET
41
+ `/boards/{boardId}/customFields`, request `customFieldItems` on cards, match
42
+ `idValue` → option text/color. Skip unless the user wants them — it's an extra call.
43
+
15
44
  ## Setup
16
45
 
17
46
  ### 1. Auth (CLI, once)
@@ -78,9 +107,9 @@ Starts stdio MCP manually (IDE normally launches `trelly-mcp` itself).
78
107
  | `trello_board_create` | Create board |
79
108
  | `trello_board_archive` | Close board (reversible) |
80
109
  | `trello_board_lists` | Lists on board |
81
- | `trello_board_cards` | All cards on board |
110
+ | `trello_board_cards` | All cards on board (**returns `display` — paste for users**) |
82
111
  | `trello_list_create` | Create list |
83
- | `trello_list_cards` | Cards in list |
112
+ | `trello_list_cards` | Cards in list (**returns `display` markdown — paste for users**) |
84
113
  | `trello_card_get` | Card (`fields` optional) |
85
114
  | `trello_card_create` | Create card on list |
86
115
  | `trello_card_update` | Update card fields map |
package/src/index.test.ts CHANGED
@@ -7,6 +7,7 @@ import { customFieldChips } from "./cli/ui/custom-fields.ts";
7
7
  import { dueStatus, labelHex, listAccentHex } from "./cli/ui/palette.ts";
8
8
  import { isBoard, isCard, isLabel, isList } from "./cli/ui/shapes.ts";
9
9
  import { attachmentMime } from "./util/attachment.ts";
10
+ import { formatCardLine, formatCardListMarkdown } from "./util/card-display.ts";
10
11
 
11
12
  describe("parseKvPairs", () => {
12
13
  it("parses key=value pairs", () => {
@@ -124,3 +125,30 @@ describe("ui shapes", () => {
124
125
  assert.ok(isBoard(board) && !isBoard(card) && !isBoard(list));
125
126
  });
126
127
  });
128
+
129
+ describe("card display markdown", () => {
130
+ it("formats linked title with labels and non-zero badges only", () => {
131
+ const line = formatCardLine({
132
+ name: "Publish module behavior",
133
+ shortUrl: "https://trello.com/c/1jH2UTAu",
134
+ labels: [{ name: "Backend", color: "blue" }],
135
+ badges: { comments: 2, attachments: 2, checkItems: 0, checkItemsChecked: 0 },
136
+ });
137
+ assert.match(
138
+ line,
139
+ /^\[Publish module behavior\]\(https:\/\/trello\.com\/c\/1jH2UTAu\)/,
140
+ );
141
+ assert.match(line, /🔵 `Backend`/);
142
+ assert.match(line, /💬 2/);
143
+ assert.match(line, /📎 2/);
144
+ assert.doesNotMatch(line, /✓/);
145
+ });
146
+
147
+ it("builds numbered list with optional heading", () => {
148
+ const md = formatCardListMarkdown(
149
+ [{ name: "A", shortUrl: "https://trello.com/c/a" }],
150
+ "**To do**",
151
+ );
152
+ assert.equal(md, "**To do**\n\n1. [A](https://trello.com/c/a)");
153
+ });
154
+ });
@@ -2,11 +2,19 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { z } from "zod";
3
3
  import { type JsonValue, type TrelloClient, TrelloError } from "../api/client.ts";
4
4
  import { getClient } from "../cli/context.ts";
5
+ import {
6
+ CARD_LIST_DISPLAY_FORMAT,
7
+ type CardDisplayInput,
8
+ formatCardListMarkdown,
9
+ } from "../util/card-display.ts";
5
10
 
6
11
  export const toolEnvelopeSchema = z.object({
7
12
  ok: z.boolean(),
8
13
  profile: z.string().optional(),
9
14
  data: z.unknown().optional(),
15
+ /** Pre-rendered markdown card list — agents MUST show this to users when present. */
16
+ display: z.string().optional(),
17
+ displayFormat: z.string().optional(),
10
18
  error: z.string().optional(),
11
19
  status: z.number().optional(),
12
20
  details: z.unknown().optional(),
@@ -17,13 +25,104 @@ export type ToolEnvelope = z.infer<typeof toolEnvelopeSchema>;
17
25
  export const profileField = z.string().optional().describe("Auth profile name");
18
26
 
19
27
  export function toolResult(envelope: ToolEnvelope): CallToolResult {
28
+ const text =
29
+ envelope.display && envelope.ok
30
+ ? `${envelope.display}\n\n---\n${JSON.stringify(envelope)}`
31
+ : JSON.stringify(envelope);
20
32
  return {
21
33
  structuredContent: envelope,
22
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
34
+ content: [{ type: "text", text }],
23
35
  isError: envelope.ok === false,
24
36
  };
25
37
  }
26
38
 
39
+ /** Attach markdown-v1 card list for agent display (see skills/trelly-mcp). */
40
+ export function cardListDisplay(
41
+ cards: CardDisplayInput[],
42
+ heading?: string,
43
+ ): Pick<ToolEnvelope, "display" | "displayFormat"> {
44
+ if (cards.length === 0) {
45
+ return {
46
+ display: heading ? `${heading}\n\n(no cards)` : "(no cards)",
47
+ displayFormat: CARD_LIST_DISPLAY_FORMAT,
48
+ };
49
+ }
50
+ return {
51
+ display: formatCardListMarkdown(cards, heading),
52
+ displayFormat: CARD_LIST_DISPLAY_FORMAT,
53
+ };
54
+ }
55
+
56
+ // Trello's badges object is ~400 chars of mostly unused keys; keep the 4 that matter.
57
+ const BADGE_KEYS = ["comments", "attachments", "checkItems", "checkItemsChecked"];
58
+
59
+ function slimCard(card: Record<string, unknown>): Record<string, unknown> {
60
+ const out = { ...card };
61
+ if (out.badges && typeof out.badges === "object" && !Array.isArray(out.badges)) {
62
+ const badges = out.badges as Record<string, unknown>;
63
+ out.badges = Object.fromEntries(
64
+ BADGE_KEYS.filter((key) => badges[key] !== undefined).map((key) => [
65
+ key,
66
+ badges[key],
67
+ ]),
68
+ );
69
+ }
70
+ if (Array.isArray(out.labels)) {
71
+ out.labels = out.labels.map((label) => {
72
+ const { id, name, color } = label as Record<string, unknown>;
73
+ return { id, name, color };
74
+ });
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /** Slim badges/labels on a card or card array returned by Trello. */
80
+ export function slimCards(data: unknown): unknown {
81
+ if (Array.isArray(data)) {
82
+ return data.map((item) =>
83
+ item && typeof item === "object"
84
+ ? slimCard(item as Record<string, unknown>)
85
+ : item,
86
+ );
87
+ }
88
+ if (data && typeof data === "object") {
89
+ return slimCard(data as Record<string, unknown>);
90
+ }
91
+ return data;
92
+ }
93
+
94
+ export async function withCardListResult(
95
+ profile: string | undefined,
96
+ fn: (client: TrelloClient) => Promise<unknown>,
97
+ heading?: string,
98
+ ): Promise<CallToolResult> {
99
+ try {
100
+ const { profileName, client } = getClient(profile);
101
+ const raw = await fn(client);
102
+ const cards = slimCards(raw);
103
+ const arr = Array.isArray(cards) ? (cards as CardDisplayInput[]) : [];
104
+ return toolResult({
105
+ ok: true,
106
+ profile: profileName,
107
+ data: arr,
108
+ ...cardListDisplay(arr, heading),
109
+ });
110
+ } catch (err) {
111
+ if (err instanceof TrelloError) {
112
+ return toolResult({
113
+ ok: false,
114
+ error: err.message,
115
+ status: err.status,
116
+ details: err.body,
117
+ });
118
+ }
119
+ return toolResult({
120
+ ok: false,
121
+ error: err instanceof Error ? err.message : String(err),
122
+ });
123
+ }
124
+ }
125
+
27
126
  export async function withClient<T>(
28
127
  profile: string | undefined,
29
128
  fn: (client: TrelloClient, profileName: string) => Promise<T>,
@@ -1,6 +1,11 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { profileField, toolEnvelopeSchema, withClient } from "../handlers.ts";
3
+ import {
4
+ profileField,
5
+ toolEnvelopeSchema,
6
+ withCardListResult,
7
+ withClient,
8
+ } from "../handlers.ts";
4
9
 
5
10
  export function registerBoardTools(server: McpServer): void {
6
11
  const outputSchema = toolEnvelopeSchema;
@@ -25,7 +30,10 @@ export function registerBoardTools(server: McpServer): void {
25
30
  ])
26
31
  .optional()
27
32
  .default("open"),
28
- fields: z.string().optional(),
33
+ fields: z
34
+ .string()
35
+ .default("id,name,shortUrl,closed")
36
+ .describe('comma-separated fields, "all" for everything'),
29
37
  },
30
38
  annotations: { readOnlyHint: true },
31
39
  outputSchema,
@@ -41,7 +49,10 @@ export function registerBoardTools(server: McpServer): void {
41
49
  inputSchema: {
42
50
  profile: profileField,
43
51
  boardId: z.string().min(1),
44
- fields: z.string().optional(),
52
+ fields: z
53
+ .string()
54
+ .default("id,name,desc,shortUrl,closed")
55
+ .describe('comma-separated fields, "all" for everything'),
45
56
  },
46
57
  annotations: { readOnlyHint: true },
47
58
  outputSchema,
@@ -94,23 +105,42 @@ export function registerBoardTools(server: McpServer): void {
94
105
  profile: profileField,
95
106
  boardId: z.string().min(1),
96
107
  filter: z.string().optional().default("open"),
108
+ fields: z
109
+ .string()
110
+ .default("id,name,closed,pos")
111
+ .describe('comma-separated fields, "all" for everything'),
97
112
  },
98
113
  annotations: { readOnlyHint: true },
99
114
  outputSchema,
100
115
  },
101
- async ({ profile, boardId, filter }) =>
102
- withClient(profile, (client) => client.boardLists(boardId, { filter })),
116
+ async ({ profile, boardId, filter, fields }) =>
117
+ withClient(profile, (client) => client.boardLists(boardId, { filter, fields })),
103
118
  );
104
119
 
105
120
  server.registerTool(
106
121
  "trello_board_cards",
107
122
  {
108
- description: "List all cards on a board.",
109
- inputSchema: { profile: profileField, boardId: z.string().min(1) },
123
+ description:
124
+ "List all cards on a board. Default fields omit badges/labels — pass fields including badges,labels for rich `display`. When showing cards to the user, paste response `display` verbatim.",
125
+ inputSchema: {
126
+ profile: profileField,
127
+ boardId: z.string().min(1),
128
+ fields: z
129
+ .string()
130
+ .default("id,name,idList,due,dueComplete,shortUrl,closed,badges,labels")
131
+ .describe(
132
+ 'comma-separated fields, "all" for everything; badges,labels included by default for display',
133
+ ),
134
+ displayHeading: z.string().optional(),
135
+ },
110
136
  annotations: { readOnlyHint: true },
111
137
  outputSchema,
112
138
  },
113
- async ({ profile, boardId }) =>
114
- withClient(profile, (client) => client.boardCards(boardId)),
139
+ async ({ profile, boardId, fields, displayHeading }) =>
140
+ withCardListResult(
141
+ profile,
142
+ (client) => client.boardCards(boardId, { fields }),
143
+ displayHeading,
144
+ ),
115
145
  );
116
146
  }
@@ -1,6 +1,11 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { profileField, toolEnvelopeSchema, withClient } from "../handlers.ts";
3
+ import {
4
+ profileField,
5
+ slimCards,
6
+ toolEnvelopeSchema,
7
+ withClient,
8
+ } from "../handlers.ts";
4
9
 
5
10
  export function registerCardTools(server: McpServer): void {
6
11
  const outputSchema = toolEnvelopeSchema;
@@ -12,13 +17,18 @@ export function registerCardTools(server: McpServer): void {
12
17
  inputSchema: {
13
18
  profile: profileField,
14
19
  cardId: z.string().min(1),
15
- fields: z.string().optional(),
20
+ fields: z
21
+ .string()
22
+ .default("id,name,desc,due,dueComplete,idList,shortUrl,labels,badges")
23
+ .describe('comma-separated fields, "all" for everything'),
16
24
  },
17
25
  annotations: { readOnlyHint: true },
18
26
  outputSchema,
19
27
  },
20
28
  async ({ profile, cardId, fields }) =>
21
- withClient(profile, (client) => client.cardGet(cardId, { fields })),
29
+ withClient(profile, async (client) =>
30
+ slimCards(await client.cardGet(cardId, { fields })),
31
+ ),
22
32
  );
23
33
 
24
34
  server.registerTool(
@@ -1,6 +1,11 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { profileField, toolEnvelopeSchema, withClient } from "../handlers.ts";
3
+ import {
4
+ profileField,
5
+ toolEnvelopeSchema,
6
+ withCardListResult,
7
+ withClient,
8
+ } from "../handlers.ts";
4
9
 
5
10
  export function registerListTools(server: McpServer): void {
6
11
  const outputSchema = toolEnvelopeSchema;
@@ -26,12 +31,30 @@ export function registerListTools(server: McpServer): void {
26
31
  server.registerTool(
27
32
  "trello_list_cards",
28
33
  {
29
- description: "List cards in a list.",
30
- inputSchema: { profile: profileField, listId: z.string().min(1) },
34
+ description:
35
+ "List cards in a list. Response includes `display` (markdown-v1, linked titles + 💬📎✓⏰ badges) — when the user should SEE cards, paste `display` verbatim; do not reformat as a plain title list.",
36
+ inputSchema: {
37
+ profile: profileField,
38
+ listId: z.string().min(1),
39
+ fields: z
40
+ .string()
41
+ .default("id,name,idList,due,dueComplete,shortUrl,closed,badges,labels")
42
+ .describe('comma-separated fields, "all" for everything'),
43
+ displayHeading: z
44
+ .string()
45
+ .optional()
46
+ .describe(
47
+ 'Optional markdown heading prepended to `display` (e.g. "**Dogster → To do**")',
48
+ ),
49
+ },
31
50
  annotations: { readOnlyHint: true },
32
51
  outputSchema,
33
52
  },
34
- async ({ profile, listId }) =>
35
- withClient(profile, (client) => client.listCards(listId)),
53
+ async ({ profile, listId, fields, displayHeading }) =>
54
+ withCardListResult(
55
+ profile,
56
+ (client) => client.listCards(listId, { fields }),
57
+ displayHeading,
58
+ ),
36
59
  );
37
60
  }
@@ -15,16 +15,28 @@ export function registerSearchTools(server: McpServer): void {
15
15
  modelTypes: z.string().optional(),
16
16
  cardsLimit: z.number().int().positive().optional(),
17
17
  boardsLimit: z.number().int().positive().optional(),
18
+ cardFields: z.string().default("id,name,idList,shortUrl"),
19
+ boardFields: z.string().default("id,name,shortUrl"),
18
20
  },
19
21
  annotations: { readOnlyHint: true },
20
22
  outputSchema,
21
23
  },
22
- async ({ profile, query, modelTypes, cardsLimit, boardsLimit }) =>
24
+ async ({
25
+ profile,
26
+ query,
27
+ modelTypes,
28
+ cardsLimit,
29
+ boardsLimit,
30
+ cardFields,
31
+ boardFields,
32
+ }) =>
23
33
  withClient(profile, (client) =>
24
34
  client.search(query, {
25
35
  modelTypes,
26
36
  cards_limit: cardsLimit,
27
37
  boards_limit: boardsLimit,
38
+ card_fields: cardFields,
39
+ board_fields: boardFields,
28
40
  }),
29
41
  ),
30
42
  );
@@ -0,0 +1,100 @@
1
+ import { dueStatus, formatDue } from "../cli/ui/palette.ts";
2
+
3
+ /** Markdown emoji for Trello label colors (agent-facing card lists). */
4
+ const LABEL_DOT: Record<string, string> = {
5
+ red: "🔴",
6
+ orange: "🟠",
7
+ yellow: "🟡",
8
+ green: "🟢",
9
+ lime: "🟢",
10
+ blue: "🔵",
11
+ sky: "🔵",
12
+ sky_light: "🔵",
13
+ sky_dark: "🔵",
14
+ purple: "🟣",
15
+ pink: "🟣",
16
+ black: "⚫",
17
+ none: "⚪",
18
+ };
19
+
20
+ export type CardDisplayInput = {
21
+ name?: string;
22
+ shortUrl?: string;
23
+ url?: string;
24
+ due?: string | null;
25
+ dueComplete?: boolean;
26
+ badges?: {
27
+ comments?: number;
28
+ attachments?: number;
29
+ checkItems?: number;
30
+ checkItemsChecked?: number;
31
+ };
32
+ labels?: Array<{ name?: string; color?: string | null }>;
33
+ };
34
+
35
+ function labelDot(color: string | null | undefined): string {
36
+ if (!color) return "⚪";
37
+ const base = color.split("_")[0] ?? color;
38
+ return LABEL_DOT[color] ?? LABEL_DOT[base] ?? "⚪";
39
+ }
40
+
41
+ function labelParts(labels: CardDisplayInput["labels"]): string[] {
42
+ if (!labels?.length) return [];
43
+ return labels
44
+ .map((label) => {
45
+ const name = (label.name ?? "").trim();
46
+ const dot = labelDot(label.color);
47
+ return name ? `${dot} \`${name}\`` : dot;
48
+ })
49
+ .filter(Boolean);
50
+ }
51
+
52
+ function badgeParts(badges: CardDisplayInput["badges"]): string[] {
53
+ if (!badges) return [];
54
+ const parts: string[] = [];
55
+ const comments = badges.comments ?? 0;
56
+ const attachments = badges.attachments ?? 0;
57
+ const total = badges.checkItems ?? 0;
58
+ const checked = badges.checkItemsChecked ?? 0;
59
+ if (comments > 0) parts.push(`💬 ${comments}`);
60
+ if (attachments > 0) parts.push(`📎 ${attachments}`);
61
+ if (total > 0) parts.push(`✓ ${checked}/${total}`);
62
+ return parts;
63
+ }
64
+
65
+ function duePart(
66
+ due: string | null | undefined,
67
+ dueComplete: boolean | undefined,
68
+ ): string {
69
+ if (!due) return "";
70
+ const status = dueStatus(due, dueComplete);
71
+ const label = formatDue(due);
72
+ if (status === "complete") return `⏰ ${label} ✓`;
73
+ if (status === "overdue") return `⏰ ${label} (overdue)`;
74
+ return `⏰ ${label}`;
75
+ }
76
+
77
+ /** One markdown line: `[name](shortUrl)` + labels + non-zero badges + due. */
78
+ export function formatCardLine(card: CardDisplayInput, index?: number): string {
79
+ const name = card.name ?? "(untitled)";
80
+ const href = card.shortUrl ?? card.url ?? "";
81
+ const prefix = index === undefined ? "" : `${index}. `;
82
+ const title = href ? `[${name}](${href})` : name;
83
+ const extras = [...labelParts(card.labels), ...badgeParts(card.badges)];
84
+ const due = duePart(card.due, card.dueComplete);
85
+ if (due) extras.push(due);
86
+ if (extras.length === 0) return `${prefix}${title}`;
87
+ return `${prefix}${title} · ${extras.join(" · ")}`;
88
+ }
89
+
90
+ /** Numbered markdown list for agent/user display (`displayFormat: markdown-v1`). */
91
+ export function formatCardListMarkdown(
92
+ cards: CardDisplayInput[],
93
+ heading?: string,
94
+ ): string {
95
+ const lines = cards.map((card, i) => formatCardLine(card, i + 1));
96
+ if (!heading) return lines.join("\n");
97
+ return `${heading}\n\n${lines.join("\n")}`;
98
+ }
99
+
100
+ export const CARD_LIST_DISPLAY_FORMAT = "markdown-v1";