@denisixnpm/planka-mcp 2.3.0 → 2.4.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.
- package/README.md +65 -5
- package/dist/condense.js +7 -2
- package/dist/server.js +200 -2
- package/dist/tools/core/index.d.ts +2 -2
- package/dist/tools/core/index.js +3 -2
- package/dist/tools/core/tools.d.ts +14 -0
- package/dist/tools/core/tools.js +76 -5
- package/dist/tools/optional/index.d.ts +2 -2
- package/dist/tools/optional/index.js +2 -3
- package/dist/tools/optional/tools.d.ts +0 -4
- package/dist/tools/optional/tools.js +0 -43
- package/dist/tools/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,8 +11,25 @@ No build step, no Docker required — `npx` runs the prebuilt package over stdio
|
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
|
+
Two auth modes: **username/password** works on every Planka version (the server logs in
|
|
15
|
+
automatically, caches the token, and silently re-logins on expiry); **API key** is Planka **v2
|
|
16
|
+
only** — generate one in your Planka user settings → *API keys* (v1.26.x has no API keys; verified:
|
|
17
|
+
`X-Api-Key` returns 401 there).
|
|
18
|
+
|
|
14
19
|
### Claude Code
|
|
15
20
|
|
|
21
|
+
**Planka v1 (≤ 1.26.x) — username/password:**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
claude mcp add planka \
|
|
25
|
+
--env PLANKA_BASE_URL=http://localhost:3000 \
|
|
26
|
+
--env PLANKA_USERNAME=your-email-or-username \
|
|
27
|
+
--env PLANKA_PASSWORD=your-password \
|
|
28
|
+
-- npx -y @denisixnpm/planka-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Planka v2 — API key (username/password above works too):**
|
|
32
|
+
|
|
16
33
|
```bash
|
|
17
34
|
claude mcp add planka \
|
|
18
35
|
--env PLANKA_BASE_URL=http://localhost:3000 \
|
|
@@ -20,7 +37,7 @@ claude mcp add planka \
|
|
|
20
37
|
-- npx -y @denisixnpm/planka-mcp
|
|
21
38
|
```
|
|
22
39
|
|
|
23
|
-
Or in `claude_desktop_config.json` / `.mcp.json
|
|
40
|
+
Or in `claude_desktop_config.json` / `.mcp.json` (username/password form — works on both versions):
|
|
24
41
|
|
|
25
42
|
```json
|
|
26
43
|
{
|
|
@@ -28,7 +45,7 @@ Or in `claude_desktop_config.json` / `.mcp.json`:
|
|
|
28
45
|
"planka": {
|
|
29
46
|
"command": "npx",
|
|
30
47
|
"args": ["-y", "@denisixnpm/planka-mcp"],
|
|
31
|
-
"env": { "PLANKA_BASE_URL": "http://localhost:3000", "
|
|
48
|
+
"env": { "PLANKA_BASE_URL": "http://localhost:3000", "PLANKA_USERNAME": "your-email-or-username", "PLANKA_PASSWORD": "your-password" }
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
}
|
|
@@ -36,6 +53,17 @@ Or in `claude_desktop_config.json` / `.mcp.json`:
|
|
|
36
53
|
|
|
37
54
|
### Codex (`~/.codex/config.toml`)
|
|
38
55
|
|
|
56
|
+
**Planka v1 (≤ 1.26.x):**
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[mcp_servers.planka]
|
|
60
|
+
command = "npx"
|
|
61
|
+
args = ["-y", "@denisixnpm/planka-mcp"]
|
|
62
|
+
env = { PLANKA_BASE_URL = "http://localhost:3000", PLANKA_USERNAME = "your-email-or-username", PLANKA_PASSWORD = "your-password" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Planka v2 (API key):**
|
|
66
|
+
|
|
39
67
|
```toml
|
|
40
68
|
[mcp_servers.planka]
|
|
41
69
|
command = "npx"
|
|
@@ -45,6 +73,22 @@ env = { PLANKA_BASE_URL = "http://localhost:3000", PLANKA_API_KEY = "your-api-ke
|
|
|
45
73
|
|
|
46
74
|
### opencode (`opencode.json`)
|
|
47
75
|
|
|
76
|
+
**Planka v1 (≤ 1.26.x):**
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcp": {
|
|
81
|
+
"planka": {
|
|
82
|
+
"type": "local",
|
|
83
|
+
"command": ["npx", "-y", "@denisixnpm/planka-mcp"],
|
|
84
|
+
"environment": { "PLANKA_BASE_URL": "http://localhost:3000", "PLANKA_USERNAME": "your-email-or-username", "PLANKA_PASSWORD": "your-password" }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Planka v2 (API key):**
|
|
91
|
+
|
|
48
92
|
```json
|
|
49
93
|
{
|
|
50
94
|
"mcp": {
|
|
@@ -59,18 +103,34 @@ env = { PLANKA_BASE_URL = "http://localhost:3000", PLANKA_API_KEY = "your-api-ke
|
|
|
59
103
|
|
|
60
104
|
Requires Node.js ≥ 22.19 (or run the same command with Bun ≥ 1.3). Debug: `npx -y @denisixnpm/planka-mcp 2>&1 | tee debug.log`.
|
|
61
105
|
|
|
106
|
+
## Attachments
|
|
107
|
+
|
|
108
|
+
`cards.get` returns the full working context in one call: description, tasks, task lists,
|
|
109
|
+
attachments, and the activity log (`actions`). Attachment metadata is normalized across Planka
|
|
110
|
+
versions (v2's nested `data.url` is flattened), so every record carries `id`, `name`, `url`,
|
|
111
|
+
`type`, `size`, `mimeType`.
|
|
112
|
+
|
|
113
|
+
- **Create with files**: `cards { action: "create", ..., data: { attachments: [{ name, text | contentBase64, mimeType? }] } }` — uploads happen in the same tool call.
|
|
114
|
+
- **Upload later**: `attachments { action: "upload", id: <cardId>, data: { name, text | contentBase64 | url, mimeType? } }` — `url` creates a link attachment (Planka v2 only).
|
|
115
|
+
- **Download**: `attachments { action: "download", id: <attachmentId>, data: { cardId } }` — returns `{ name, mimeType, size, contentBase64 }` (≤ 10 MB inline).
|
|
116
|
+
- **Rename / delete**: `attachments { action: "rename" | "delete", id, data: { name? } }`.
|
|
117
|
+
|
|
118
|
+
Downloads require `PLANKA_USERNAME`/`PLANKA_PASSWORD` auth: Planka serves files from a
|
|
119
|
+
cookie-authenticated route that rejects `Authorization` headers and API keys (verified on 1.26.2
|
|
120
|
+
and 2.0.3).
|
|
121
|
+
|
|
62
122
|
## Configuration
|
|
63
123
|
|
|
64
124
|
| Variable | Default | Description |
|
|
65
125
|
|----------|---------|-------------|
|
|
66
126
|
| `PLANKA_BASE_URL` | `http://localhost:3000` | Planka instance URL |
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
127
|
+
| `PLANKA_USERNAME` / `PLANKA_PASSWORD` | — | Login auth — **all Planka versions**; token is fetched, cached, and refreshed automatically |
|
|
128
|
+
| `PLANKA_API_KEY` | — | API key auth — **Planka v2 only** (user settings → API keys); wins if both are set |
|
|
69
129
|
| `PLANKA_HTTP_TIMEOUT_MS` | `30000` | Per-request timeout (aborts, then retries) |
|
|
70
130
|
| `PLANKA_HTTP_MAX_RETRIES` | `2` | Retries for `408`/`429`/`5xx` and network errors |
|
|
71
131
|
| `PLANKA_HTTP_RETRY_BASE_DELAY_MS` | `250` | Exponential backoff base (`base * 2^attempt`) |
|
|
72
132
|
| `PLANKA_CONDENSED_OUTPUT` | `true` | Condensed agent output; `false` = raw payloads |
|
|
73
|
-
| `ENABLE_ALL_TOOLS` | `false` | Enable all 28 tools (otherwise
|
|
133
|
+
| `ENABLE_ALL_TOOLS` | `false` | Enable all 28 tools (otherwise 12 core) |
|
|
74
134
|
| `ENABLE_ADMIN_TOOLS` / `ENABLE_OPTIONAL_TOOLS` | `false` | Enable one extra category |
|
|
75
135
|
| `MCP_TRANSPORT` | `stdio` | `stdio` (single client) or `sse` (multi-client) |
|
|
76
136
|
| `MCP_PORT` / `MCP_HOST` | `3001` / `127.0.0.1` | SSE mode bind; `0.0.0.0` in Docker |
|
package/dist/condense.js
CHANGED
|
@@ -30,7 +30,7 @@ const ENTITY_FIELDS = {
|
|
|
30
30
|
labelExtras: ["id", "cardId", "labelId"],
|
|
31
31
|
comments: ["id", "text", "cardId"],
|
|
32
32
|
commentExtras: ["id", "text", "cardId"],
|
|
33
|
-
attachments: ["id", "name", "url", "type", "cardId"],
|
|
33
|
+
attachments: ["id", "name", "url", "type", "size", "mimeType", "cardId"],
|
|
34
34
|
users: ["id", "email", "name", "username", "role"],
|
|
35
35
|
userInfo: ["id", "email", "name", "username", "role"],
|
|
36
36
|
boardMembers: ["id", "userId", "boardId", "role"],
|
|
@@ -44,7 +44,7 @@ const ENTITY_FIELDS = {
|
|
|
44
44
|
webhooks: ["id", "name", "url"],
|
|
45
45
|
backgroundImages: ["id", "name", "boardId"],
|
|
46
46
|
notifications: ["id", "type", "isRead", "cardId"],
|
|
47
|
-
actions: ["id", "type", "cardId", "userId"],
|
|
47
|
+
actions: ["id", "type", "cardId", "boardId", "userId", "createdAt", "data"],
|
|
48
48
|
};
|
|
49
49
|
/** `included` keys that do not match their tool name verbatim. */
|
|
50
50
|
const INCLUDED_KEY_TO_TOOL = {
|
|
@@ -103,6 +103,11 @@ export function condenseResult(toolName, data) {
|
|
|
103
103
|
}
|
|
104
104
|
if (typeof data === "object" && data !== null) {
|
|
105
105
|
const source = data;
|
|
106
|
+
// Attachment downloads carry base64 payload — whitelists and text
|
|
107
|
+
// truncation must not touch them (size is already capped by the engine).
|
|
108
|
+
if (typeof source.contentBase64 === "string") {
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
106
111
|
const hasItems = Array.isArray(source.items);
|
|
107
112
|
const hasItem = "item" in source;
|
|
108
113
|
if (hasItems || hasItem) {
|
package/dist/server.js
CHANGED
|
@@ -138,6 +138,11 @@ async function executeGroupedApiCall(groupedDef, input, overridePath, retryAttem
|
|
|
138
138
|
const validActions = Object.keys(groupedDef.operations).join(", ");
|
|
139
139
|
return { success: false, error: `Invalid action '${action}'. Valid actions: ${validActions}` };
|
|
140
140
|
}
|
|
141
|
+
// Fully custom operations (attachment download needs cookie-auth + binary
|
|
142
|
+
// handling that the generic JSON engine cannot express).
|
|
143
|
+
if (operation.custom === "downloadAttachment") {
|
|
144
|
+
return downloadAttachment(input, scope);
|
|
145
|
+
}
|
|
141
146
|
// Construct URL with path parameters
|
|
142
147
|
const canonicalPath = overridePath ?? operation.path;
|
|
143
148
|
let actualPath = canonicalPath;
|
|
@@ -201,7 +206,38 @@ async function executeGroupedApiCall(groupedDef, input, overridePath, retryAttem
|
|
|
201
206
|
}
|
|
202
207
|
// Handle request body
|
|
203
208
|
let body = undefined;
|
|
204
|
-
if (
|
|
209
|
+
if (operation.requestType === "multipart") {
|
|
210
|
+
// Planka file upload: v2 requires form fields `type` + `name`; v1
|
|
211
|
+
// ignores extra fields, so one v2-shaped request serves both majors.
|
|
212
|
+
const d = (input?.data ?? {});
|
|
213
|
+
const name = typeof d.name === "string" && d.name !== "" ? d.name : "attachment";
|
|
214
|
+
const isLink = typeof d.url === "string" && d.url !== "" && d.contentBase64 === undefined && d.text === undefined;
|
|
215
|
+
const fd = new FormData();
|
|
216
|
+
fd.append("type", isLink ? "link" : "file");
|
|
217
|
+
fd.append("name", name);
|
|
218
|
+
if (isLink) {
|
|
219
|
+
fd.append("url", String(d.url));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
let bytes;
|
|
223
|
+
let mimeType = typeof d.mimeType === "string" ? d.mimeType : "application/octet-stream";
|
|
224
|
+
if (typeof d.contentBase64 === "string" && d.contentBase64 !== "") {
|
|
225
|
+
const binary = atob(d.contentBase64);
|
|
226
|
+
bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0));
|
|
227
|
+
}
|
|
228
|
+
else if (typeof d.text === "string") {
|
|
229
|
+
bytes = new TextEncoder().encode(d.text);
|
|
230
|
+
if (typeof d.mimeType !== "string")
|
|
231
|
+
mimeType = "text/plain";
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
return { success: false, error: "Provide data.contentBase64, data.text, or data.url for the attachment content" };
|
|
235
|
+
}
|
|
236
|
+
fd.append("file", new Blob([bytes], { type: mimeType }), name);
|
|
237
|
+
}
|
|
238
|
+
body = fd;
|
|
239
|
+
}
|
|
240
|
+
else if (["POST", "PUT", "PATCH"].includes(methodUpper) && input?.data !== undefined) {
|
|
205
241
|
headers["Content-Type"] = "application/json";
|
|
206
242
|
body = JSON.stringify(input.data);
|
|
207
243
|
}
|
|
@@ -272,6 +308,152 @@ async function executeGroupedApiCall(groupedDef, input, overridePath, retryAttem
|
|
|
272
308
|
};
|
|
273
309
|
}
|
|
274
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Planka v2 nests attachment metadata under `data` ({url, size, mimeType});
|
|
313
|
+
* v1 keeps `url` top-level. Flatten to one shape for agents and the condenser.
|
|
314
|
+
*/
|
|
315
|
+
function normalizeAttachment(raw) {
|
|
316
|
+
const item = (raw ?? {});
|
|
317
|
+
const nested = item.data;
|
|
318
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
319
|
+
return {
|
|
320
|
+
...item,
|
|
321
|
+
url: item.url ?? nested.url,
|
|
322
|
+
size: nested.size,
|
|
323
|
+
mimeType: nested.mimeType,
|
|
324
|
+
data: undefined,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return item;
|
|
328
|
+
}
|
|
329
|
+
/** Normalize every attachment record inside a `{item|items, included}` envelope. */
|
|
330
|
+
function normalizeAttachmentsIn(data) {
|
|
331
|
+
if (typeof data !== "object" || data === null)
|
|
332
|
+
return;
|
|
333
|
+
const envelope = data;
|
|
334
|
+
const item = envelope.item;
|
|
335
|
+
if (item && item.type !== undefined && (item.cardId !== undefined || item.data !== undefined)) {
|
|
336
|
+
envelope.item = normalizeAttachment(item);
|
|
337
|
+
}
|
|
338
|
+
const list = envelope.included?.attachments;
|
|
339
|
+
if (Array.isArray(list)) {
|
|
340
|
+
envelope.included.attachments = list.map(normalizeAttachment);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Download an attachment's bytes. Planka serves files from
|
|
345
|
+
* /attachments/{id}/download/{name}, which authenticates via the
|
|
346
|
+
* `accessToken` cookie only — Bearer and X-Api-Key headers get 401 (verified
|
|
347
|
+
* against live 1.26.2 and 2.0.3). The attachment record must be located on
|
|
348
|
+
* its owning card first, since there is no GET /attachments/{id}.
|
|
349
|
+
*/
|
|
350
|
+
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
351
|
+
async function downloadAttachment(input, scope) {
|
|
352
|
+
if (PLANKA_API_KEY) {
|
|
353
|
+
return { success: false, error: "Attachment downloads require PLANKA_USERNAME/PLANKA_PASSWORD auth: Planka serves files from a cookie-authenticated route that rejects API keys" };
|
|
354
|
+
}
|
|
355
|
+
const attachmentId = String(input?.id ?? "");
|
|
356
|
+
const cardId = String(input?.data?.cardId ?? scope?.cardId ?? "");
|
|
357
|
+
if (!attachmentId || !cardId) {
|
|
358
|
+
return { success: false, error: "Provide id (attachment ID) and data.cardId (owning card), or select the card via the context tool" };
|
|
359
|
+
}
|
|
360
|
+
const cardsTool = toolMap.get("cards");
|
|
361
|
+
if (!cardsTool) {
|
|
362
|
+
return { success: false, error: "cards tool unavailable; cannot resolve attachment" };
|
|
363
|
+
}
|
|
364
|
+
const cardRes = await executeGroupedApiCall(cardsTool.groupedDef, { action: "get", id: cardId });
|
|
365
|
+
if (!cardRes.success) {
|
|
366
|
+
return cardRes;
|
|
367
|
+
}
|
|
368
|
+
const found = (cardRes.data?.included?.attachments ?? []).find(a => String(a.id) === attachmentId);
|
|
369
|
+
if (!found) {
|
|
370
|
+
return { success: false, error: `Attachment ${attachmentId} not found on card ${cardId}` };
|
|
371
|
+
}
|
|
372
|
+
const record = normalizeAttachment(found);
|
|
373
|
+
if (record.type === "link" || typeof record.url !== "string" || record.url === "") {
|
|
374
|
+
return { success: true, data: { ...record, note: "link attachment — follow data.url" } };
|
|
375
|
+
}
|
|
376
|
+
const target = new URL(String(record.url), PLANKA_BASE_URL);
|
|
377
|
+
const token = await getAccessToken();
|
|
378
|
+
let res;
|
|
379
|
+
try {
|
|
380
|
+
res = await fetch(target, {
|
|
381
|
+
headers: { Authorization: `Bearer ${token}`, cookie: `accessToken=${token}` },
|
|
382
|
+
signal: AbortSignal.timeout(PLANKA_HTTP_TIMEOUT_MS),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (downloadError) {
|
|
386
|
+
return { success: false, error: `Attachment download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}` };
|
|
387
|
+
}
|
|
388
|
+
if (!res.ok) {
|
|
389
|
+
return { success: false, error: `Attachment download failed: HTTP ${res.status}` };
|
|
390
|
+
}
|
|
391
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
392
|
+
if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
393
|
+
return { success: false, error: `Attachment too large to inline (${bytes.byteLength} bytes > ${MAX_ATTACHMENT_BYTES}); fetch it directly: ${target}` };
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
success: true,
|
|
397
|
+
data: {
|
|
398
|
+
id: attachmentId,
|
|
399
|
+
name: record.name,
|
|
400
|
+
size: bytes.byteLength,
|
|
401
|
+
mimeType: res.headers.get("content-type")?.split(";")[0] ?? record.mimeType ?? "application/octet-stream",
|
|
402
|
+
contentBase64: Buffer.from(bytes).toString("base64"),
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Post-processing for card calls:
|
|
408
|
+
* - `get`: fold the activity log (`actions`) into `included` so one call
|
|
409
|
+
* returns the full working context — description, tasks, attachments,
|
|
410
|
+
* actions. Best-effort: an actions failure never fails the card get.
|
|
411
|
+
* - `create`: upload any `data.attachments` entries and fold them into
|
|
412
|
+
* `included.attachments`.
|
|
413
|
+
*/
|
|
414
|
+
async function enrichCardResult(action, data, pendingAttachments, cardId, scope) {
|
|
415
|
+
normalizeAttachmentsIn(data);
|
|
416
|
+
const cardsTool = toolMap.get("cards");
|
|
417
|
+
if (action === "get" && cardsTool && cardId) {
|
|
418
|
+
try {
|
|
419
|
+
const acts = await executeGroupedApiCall(cardsTool.groupedDef, { action: "getActions", id: cardId }, undefined, 0, false, false, scope);
|
|
420
|
+
if (acts.success && Array.isArray(acts.data?.items)) {
|
|
421
|
+
if (!data.included || typeof data.included !== "object")
|
|
422
|
+
data.included = {};
|
|
423
|
+
data.included.actions = acts.data.items;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (actionsError) {
|
|
427
|
+
console.error(`[cards.get] actions fetch failed for card ${cardId}: ${actionsError instanceof Error ? actionsError.message : String(actionsError)}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (action === "create" && pendingAttachments && pendingAttachments.length > 0) {
|
|
431
|
+
const createdCardId = String(data?.item?.id ?? cardId ?? "");
|
|
432
|
+
const attachmentsTool = toolMap.get("attachments");
|
|
433
|
+
if (createdCardId && attachmentsTool) {
|
|
434
|
+
const uploaded = [];
|
|
435
|
+
const errors = [];
|
|
436
|
+
for (const att of pendingAttachments) {
|
|
437
|
+
const up = await executeGroupedApiCall(attachmentsTool.groupedDef, { action: "upload", id: createdCardId, data: att });
|
|
438
|
+
if (up.success) {
|
|
439
|
+
uploaded.push(normalizeAttachment(up.data?.item));
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
errors.push({ name: String(att?.name ?? "attachment"), error: up.error ?? "upload failed" });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (uploaded.length > 0) {
|
|
446
|
+
if (!data.included || typeof data.included !== "object")
|
|
447
|
+
data.included = {};
|
|
448
|
+
data.included.attachments = [...(data.included.attachments ?? []), ...uploaded];
|
|
449
|
+
}
|
|
450
|
+
if (errors.length > 0) {
|
|
451
|
+
data.attachmentUploadErrors = errors;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return data;
|
|
456
|
+
}
|
|
275
457
|
/**
|
|
276
458
|
* Local handler for the `context` tool: maintains the working scope of this
|
|
277
459
|
* server instance (per SSE session, process-wide for stdio). Enriches `set`
|
|
@@ -342,7 +524,7 @@ if (!process.argv.includes("--healthcheck")) {
|
|
|
342
524
|
function createMcpServer() {
|
|
343
525
|
const server = new McpServer({
|
|
344
526
|
name: "planka-mcp",
|
|
345
|
-
version: "2.
|
|
527
|
+
version: "2.4.0",
|
|
346
528
|
}, {
|
|
347
529
|
capabilities: {
|
|
348
530
|
tools: {},
|
|
@@ -366,9 +548,25 @@ function createMcpServer() {
|
|
|
366
548
|
isError: true,
|
|
367
549
|
};
|
|
368
550
|
}
|
|
551
|
+
// cards.create accepts data.attachments — strip them from the API payload
|
|
552
|
+
// and upload after the card exists.
|
|
553
|
+
let pendingAttachments = null;
|
|
554
|
+
const reqArgs = (args ?? {});
|
|
555
|
+
const reqData = reqArgs.data;
|
|
556
|
+
if (toolDef.groupedDef.name === "cards" && reqArgs.action === "create" && Array.isArray(reqData?.attachments)) {
|
|
557
|
+
pendingAttachments = reqData.attachments;
|
|
558
|
+
const { attachments: _omit, ...dataWithout } = reqData;
|
|
559
|
+
reqArgs.data = dataWithout;
|
|
560
|
+
}
|
|
369
561
|
const result = toolDef.groupedDef.name === "context"
|
|
370
562
|
? await handleContextCall(args, scope)
|
|
371
563
|
: await executeGroupedApiCall(toolDef.groupedDef, args, undefined, 0, false, true, scope);
|
|
564
|
+
if (result.success && toolDef.groupedDef.name === "attachments") {
|
|
565
|
+
normalizeAttachmentsIn(result.data);
|
|
566
|
+
}
|
|
567
|
+
if (result.success && toolDef.groupedDef.name === "cards") {
|
|
568
|
+
result.data = await enrichCardResult(String(args?.action ?? ""), result.data, pendingAttachments, typeof args?.id === "string" ? args.id : undefined, scope);
|
|
569
|
+
}
|
|
372
570
|
if (result.success) {
|
|
373
571
|
const wantsRaw = !PLANKA_CONDENSED_OUTPUT || args?.raw === true;
|
|
374
572
|
const payload = wantsRaw ? result.data : condenseResult(toolDef.groupedDef.name, result.data);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Core tools - Essential for basic Kanban operations (always enabled)
|
|
3
3
|
*/
|
|
4
4
|
import { GroupedToolDefinition } from "../types.js";
|
|
5
|
-
import { authTool, projectsTool, boardsTool, listsTool, cardsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool } from "./tools.js";
|
|
6
|
-
export { authTool, projectsTool, boardsTool, listsTool, cardsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool, };
|
|
5
|
+
import { authTool, projectsTool, boardsTool, listsTool, cardsTool, attachmentsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool } from "./tools.js";
|
|
6
|
+
export { authTool, projectsTool, boardsTool, listsTool, cardsTool, attachmentsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool, };
|
|
7
7
|
/**
|
|
8
8
|
* All core tools combined
|
|
9
9
|
*/
|
package/dist/tools/core/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { authTool, projectsTool, boardsTool, listsTool, cardsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool, } from "./tools.js";
|
|
2
|
-
export { authTool, projectsTool, boardsTool, listsTool, cardsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool, };
|
|
1
|
+
import { authTool, projectsTool, boardsTool, listsTool, cardsTool, attachmentsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool, } from "./tools.js";
|
|
2
|
+
export { authTool, projectsTool, boardsTool, listsTool, cardsTool, attachmentsTool, commentsTool, tasksTool, labelsTool, cardMembersTool, bootstrapTool, contextTool, };
|
|
3
3
|
/**
|
|
4
4
|
* All core tools combined
|
|
5
5
|
*/
|
|
@@ -11,6 +11,7 @@ export const coreTools = [
|
|
|
11
11
|
boardsTool,
|
|
12
12
|
listsTool,
|
|
13
13
|
cardsTool,
|
|
14
|
+
attachmentsTool,
|
|
14
15
|
commentsTool,
|
|
15
16
|
tasksTool,
|
|
16
17
|
labelsTool,
|
|
@@ -19,6 +19,20 @@ export declare const listsTool: GroupedToolDefinition;
|
|
|
19
19
|
* Cards tool - manages cards within lists
|
|
20
20
|
*/
|
|
21
21
|
export declare const cardsTool: GroupedToolDefinition;
|
|
22
|
+
/**
|
|
23
|
+
* Attachments tool - upload, download, and manage card file attachments.
|
|
24
|
+
*
|
|
25
|
+
* Verified against live Planka 1.26.2 and 2.0.3:
|
|
26
|
+
* - upload: POST /cards/{cardId}/attachments, multipart. v2 requires form
|
|
27
|
+
* fields `type` ("file"|"link") and `name`; v1 ignores extra fields, so a
|
|
28
|
+
* single v2-shaped request works on both.
|
|
29
|
+
* - download: no JSON API — the attachment record carries an `url`
|
|
30
|
+
* (/attachments/{id}/download/{name}) that Planka authenticates via the
|
|
31
|
+
* `accessToken` cookie only (Bearer/X-Api-Key get 401). The engine fetches
|
|
32
|
+
* it and returns base64.
|
|
33
|
+
* - link attachments (url instead of file content) are Planka v2 only.
|
|
34
|
+
*/
|
|
35
|
+
export declare const attachmentsTool: GroupedToolDefinition;
|
|
22
36
|
/**
|
|
23
37
|
* Comments tool - manages comments on cards
|
|
24
38
|
*/
|
package/dist/tools/core/tools.js
CHANGED
|
@@ -231,7 +231,12 @@ export const cardsTool = {
|
|
|
231
231
|
get: {
|
|
232
232
|
method: "GET",
|
|
233
233
|
path: "/cards/{id}",
|
|
234
|
-
description: "Get card with task lists, attachments, and custom fields",
|
|
234
|
+
description: "Get card with description, task lists, tasks, attachments, and custom fields",
|
|
235
|
+
},
|
|
236
|
+
getActions: {
|
|
237
|
+
method: "GET",
|
|
238
|
+
path: "/cards/{cardId}/actions",
|
|
239
|
+
description: "Get the card's activity log (actions: created, moved, commented, ...)",
|
|
235
240
|
},
|
|
236
241
|
create: {
|
|
237
242
|
method: "POST",
|
|
@@ -249,19 +254,20 @@ export const cardsTool = {
|
|
|
249
254
|
description: "Delete a card permanently",
|
|
250
255
|
},
|
|
251
256
|
},
|
|
252
|
-
inputSchema: buildGroupedSchema(["list", "get", "create", "update", "delete"], {
|
|
257
|
+
inputSchema: buildGroupedSchema(["list", "get", "getActions", "create", "update", "delete"], {
|
|
253
258
|
list: "Get cards from a list (requires listId)",
|
|
254
259
|
get: "Get card details by ID",
|
|
260
|
+
getActions: "Get card activity log",
|
|
255
261
|
create: "Create a new card",
|
|
256
262
|
update: "Update card properties",
|
|
257
263
|
delete: "Delete a card",
|
|
258
264
|
}, {
|
|
259
265
|
id: {
|
|
260
|
-
description: "Card ID (for get, update, delete) or List ID (for list, create)",
|
|
261
|
-
requiredFor: ["list", "get", "update", "delete"],
|
|
266
|
+
description: "Card ID (for get, getActions, update, delete) or List ID (for list, create)",
|
|
267
|
+
requiredFor: ["list", "get", "getActions", "update", "delete"],
|
|
262
268
|
},
|
|
263
269
|
data: {
|
|
264
|
-
description: "Card data: { name: string, type: 'project'|'story' (required), position: number (required for create), listId?: string (for create/move), description?: string, dueDate?: string, isDueCompleted?: boolean }",
|
|
270
|
+
description: "Card data: { name: string, type: 'project'|'story' (required), position: number (required for create), listId?: string (for create/move), description?: string, dueDate?: string, isDueCompleted?: boolean, attachments?: [{ name, contentBase64 | text | url, mimeType? }] }",
|
|
265
271
|
requiredFor: ["create", "update"],
|
|
266
272
|
properties: {
|
|
267
273
|
name: { type: "string", description: "Card name", required: true },
|
|
@@ -271,6 +277,7 @@ export const cardsTool = {
|
|
|
271
277
|
description: { type: "string", description: "Card description" },
|
|
272
278
|
dueDate: { type: "string", description: "Due date (ISO 8601)" },
|
|
273
279
|
isDueCompleted: { type: "boolean", description: "Whether the due date is completed" },
|
|
280
|
+
attachments: { type: "array", description: "Files to attach on create: [{ name, contentBase64 (base64 of file bytes) | text (plain text content) | url (link, Planka v2), mimeType? }] (Planka v1 ignores type)" },
|
|
274
281
|
},
|
|
275
282
|
},
|
|
276
283
|
query: {
|
|
@@ -280,6 +287,70 @@ export const cardsTool = {
|
|
|
280
287
|
},
|
|
281
288
|
}),
|
|
282
289
|
};
|
|
290
|
+
/**
|
|
291
|
+
* Attachments tool - upload, download, and manage card file attachments.
|
|
292
|
+
*
|
|
293
|
+
* Verified against live Planka 1.26.2 and 2.0.3:
|
|
294
|
+
* - upload: POST /cards/{cardId}/attachments, multipart. v2 requires form
|
|
295
|
+
* fields `type` ("file"|"link") and `name`; v1 ignores extra fields, so a
|
|
296
|
+
* single v2-shaped request works on both.
|
|
297
|
+
* - download: no JSON API — the attachment record carries an `url`
|
|
298
|
+
* (/attachments/{id}/download/{name}) that Planka authenticates via the
|
|
299
|
+
* `accessToken` cookie only (Bearer/X-Api-Key get 401). The engine fetches
|
|
300
|
+
* it and returns base64.
|
|
301
|
+
* - link attachments (url instead of file content) are Planka v2 only.
|
|
302
|
+
*/
|
|
303
|
+
export const attachmentsTool = {
|
|
304
|
+
name: "attachments",
|
|
305
|
+
description: "Upload, download, and manage file attachments on Planka cards. Content moves as base64 (data.contentBase64 for upload, result.contentBase64 for download).",
|
|
306
|
+
operations: {
|
|
307
|
+
upload: {
|
|
308
|
+
method: "POST",
|
|
309
|
+
path: "/cards/{cardId}/attachments",
|
|
310
|
+
requestType: "multipart",
|
|
311
|
+
description: "Upload a file (contentBase64 or text) or attach a link (url, Planka v2) to a card",
|
|
312
|
+
},
|
|
313
|
+
download: {
|
|
314
|
+
method: "GET",
|
|
315
|
+
path: "/attachments/{id}",
|
|
316
|
+
custom: "downloadAttachment",
|
|
317
|
+
description: "Download an attachment's content as base64 (requires the owning card ID)",
|
|
318
|
+
},
|
|
319
|
+
rename: {
|
|
320
|
+
method: "PATCH",
|
|
321
|
+
path: "/attachments/{id}",
|
|
322
|
+
description: "Rename an attachment",
|
|
323
|
+
},
|
|
324
|
+
delete: {
|
|
325
|
+
method: "DELETE",
|
|
326
|
+
path: "/attachments/{id}",
|
|
327
|
+
description: "Delete an attachment permanently",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
inputSchema: buildGroupedSchema(["upload", "download", "rename", "delete"], {
|
|
331
|
+
upload: "Upload a file or link to a card (id = card ID)",
|
|
332
|
+
download: "Download attachment content as base64 (id = attachment ID)",
|
|
333
|
+
rename: "Rename an attachment",
|
|
334
|
+
delete: "Delete an attachment",
|
|
335
|
+
}, {
|
|
336
|
+
id: {
|
|
337
|
+
description: "Card ID (for upload) or Attachment ID (for download, rename, delete)",
|
|
338
|
+
requiredFor: ["upload", "download", "rename", "delete"],
|
|
339
|
+
},
|
|
340
|
+
data: {
|
|
341
|
+
description: "Attachment data: upload = { name (required), contentBase64 | text | url, mimeType? }; download = { cardId? (owning card, or set via context) }; rename = { name }",
|
|
342
|
+
requiredFor: ["upload", "rename"],
|
|
343
|
+
properties: {
|
|
344
|
+
name: { type: "string", description: "File/attachment name (e.g. report.pdf)", required: true },
|
|
345
|
+
contentBase64: { type: "string", description: "Base64-encoded file content (preferred for binary)" },
|
|
346
|
+
text: { type: "string", description: "Plain-text content (alternative to contentBase64)" },
|
|
347
|
+
mimeType: { type: "string", description: "MIME type (e.g. application/pdf); default application/octet-stream, text/plain for text" },
|
|
348
|
+
url: { type: "string", description: "Link attachments (Planka v2): attach an external URL instead of file content" },
|
|
349
|
+
cardId: { type: "string", description: "Owning card ID for download (or set cardId via the context tool)" },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
};
|
|
283
354
|
/**
|
|
284
355
|
* Comments tool - manages comments on cards
|
|
285
356
|
*/
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Optional tools - Extended functionality (not essential for basic operations)
|
|
3
3
|
*/
|
|
4
4
|
import { GroupedToolDefinition } from "../types.js";
|
|
5
|
-
import { actionsTool,
|
|
6
|
-
export { actionsTool,
|
|
5
|
+
import { actionsTool, boardMembersTool, customFieldsTool, notificationsTool, backgroundImagesTool, cardExtrasTool, commentExtrasTool, listExtrasTool, taskExtrasTool, labelExtrasTool, cardMemberExtrasTool, userInfoTool } from "./tools.js";
|
|
6
|
+
export { actionsTool, boardMembersTool, customFieldsTool, notificationsTool, backgroundImagesTool, cardExtrasTool, commentExtrasTool, listExtrasTool, taskExtrasTool, labelExtrasTool, cardMemberExtrasTool, userInfoTool, };
|
|
7
7
|
/**
|
|
8
8
|
* All optional tools combined
|
|
9
9
|
*/
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { actionsTool,
|
|
2
|
-
export { actionsTool,
|
|
1
|
+
import { actionsTool, boardMembersTool, customFieldsTool, notificationsTool, backgroundImagesTool, cardExtrasTool, commentExtrasTool, listExtrasTool, taskExtrasTool, labelExtrasTool, cardMemberExtrasTool, userInfoTool, } from "./tools.js";
|
|
2
|
+
export { actionsTool, boardMembersTool, customFieldsTool, notificationsTool, backgroundImagesTool, cardExtrasTool, commentExtrasTool, listExtrasTool, taskExtrasTool, labelExtrasTool, cardMemberExtrasTool, userInfoTool, };
|
|
3
3
|
/**
|
|
4
4
|
* All optional tools combined
|
|
5
5
|
*/
|
|
6
6
|
export const optionalTools = [
|
|
7
7
|
actionsTool,
|
|
8
|
-
attachmentsTool,
|
|
9
8
|
boardMembersTool,
|
|
10
9
|
customFieldsTool,
|
|
11
10
|
notificationsTool,
|
|
@@ -3,10 +3,6 @@ import { GroupedToolDefinition } from "../types.js";
|
|
|
3
3
|
* Actions tool - action history
|
|
4
4
|
*/
|
|
5
5
|
export declare const actionsTool: GroupedToolDefinition;
|
|
6
|
-
/**
|
|
7
|
-
* Attachments tool - manages card attachments
|
|
8
|
-
*/
|
|
9
|
-
export declare const attachmentsTool: GroupedToolDefinition;
|
|
10
6
|
/**
|
|
11
7
|
* Board Members tool - manages board memberships
|
|
12
8
|
*/
|
|
@@ -30,49 +30,6 @@ export const actionsTool = {
|
|
|
30
30
|
},
|
|
31
31
|
}),
|
|
32
32
|
};
|
|
33
|
-
/**
|
|
34
|
-
* Attachments tool - manages card attachments
|
|
35
|
-
*/
|
|
36
|
-
export const attachmentsTool = {
|
|
37
|
-
name: "attachments",
|
|
38
|
-
description: "Manage attachments on Planka cards.",
|
|
39
|
-
operations: {
|
|
40
|
-
create: {
|
|
41
|
-
method: "POST",
|
|
42
|
-
path: "/cards/{cardId}/attachments",
|
|
43
|
-
description: "Add an attachment to a card",
|
|
44
|
-
},
|
|
45
|
-
update: {
|
|
46
|
-
method: "PATCH",
|
|
47
|
-
path: "/attachments/{id}",
|
|
48
|
-
description: "Update attachment properties",
|
|
49
|
-
},
|
|
50
|
-
delete: {
|
|
51
|
-
method: "DELETE",
|
|
52
|
-
path: "/attachments/{id}",
|
|
53
|
-
description: "Delete an attachment",
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
inputSchema: buildGroupedSchema(["create", "update", "delete"], {
|
|
57
|
-
create: "Add attachment to card",
|
|
58
|
-
update: "Update attachment",
|
|
59
|
-
delete: "Delete attachment",
|
|
60
|
-
}, {
|
|
61
|
-
id: {
|
|
62
|
-
description: "Card ID (for create) or Attachment ID (for update, delete)",
|
|
63
|
-
requiredFor: ["create", "update", "delete"],
|
|
64
|
-
},
|
|
65
|
-
data: {
|
|
66
|
-
description: "Attachment data: { type: 'file'|'link', name?: string, url?: string (for link) }. Binary file uploads are not supported by this MCP server; use type 'link'.",
|
|
67
|
-
requiredFor: ["create", "update"],
|
|
68
|
-
properties: {
|
|
69
|
-
type: { type: "string", enum: ["file", "link"], description: "Attachment type", required: true },
|
|
70
|
-
name: { type: "string", description: "Attachment name" },
|
|
71
|
-
url: { type: "string", description: "URL (for link attachments)" },
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
}),
|
|
75
|
-
};
|
|
76
33
|
/**
|
|
77
34
|
* Board Members tool - manages board memberships
|
|
78
35
|
*/
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export interface ToolOperation {
|
|
|
14
14
|
requiresAuth?: boolean;
|
|
15
15
|
/** Description of this specific operation */
|
|
16
16
|
description?: string;
|
|
17
|
+
/** Send data as multipart/form-data instead of JSON (file uploads). */
|
|
18
|
+
requestType?: "multipart";
|
|
19
|
+
/** Fully custom handling in the engine (attachments download). */
|
|
20
|
+
custom?: "downloadAttachment";
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Grouped tool definition - multiple operations under one tool
|