@denisixnpm/planka-mcp 2.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.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/condense.d.ts +18 -0
- package/dist/condense.js +127 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +567 -0
- package/dist/tools/admin/index.d.ts +10 -0
- package/dist/tools/admin/index.js +11 -0
- package/dist/tools/admin/tools.d.ts +17 -0
- package/dist/tools/admin/tools.js +211 -0
- package/dist/tools/core/index.d.ts +10 -0
- package/dist/tools/core/index.js +18 -0
- package/dist/tools/core/tools.d.ts +46 -0
- package/dist/tools/core/tools.js +535 -0
- package/dist/tools/index.d.ts +45 -0
- package/dist/tools/index.js +61 -0
- package/dist/tools/optional/index.d.ts +10 -0
- package/dist/tools/optional/index.js +20 -0
- package/dist/tools/optional/tools.d.ts +53 -0
- package/dist/tools/optional/tools.js +578 -0
- package/dist/tools/types.d.ts +61 -0
- package/dist/tools/types.js +64 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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,157 @@
|
|
|
1
|
+
# Planka MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@denisixnpm/planka-mcp)
|
|
4
|
+
[](https://github.com/denisix/planka-mcp/releases)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
MCP server for [Planka](https://planka.app/) (real-time Kanban), tuned for AI coding agents:
|
|
8
|
+
**Claude Code, Codex, opencode** and any MCP client. Works with Planka v2 and v1 (≤ 1.26.x).
|
|
9
|
+
No build step, no Docker required — `npx` runs the prebuilt package over stdio.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
### Claude Code
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add planka \
|
|
17
|
+
--env PLANKA_BASE_URL=http://localhost:3000 \
|
|
18
|
+
--env PLANKA_API_KEY=your-api-key \
|
|
19
|
+
-- npx -y @denisixnpm/planka-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or in `claude_desktop_config.json` / `.mcp.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"planka": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "@denisixnpm/planka-mcp"],
|
|
30
|
+
"env": { "PLANKA_BASE_URL": "http://localhost:3000", "PLANKA_API_KEY": "your-api-key" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Codex (`~/.codex/config.toml`)
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
[mcp_servers.planka]
|
|
40
|
+
command = "npx"
|
|
41
|
+
args = ["-y", "@denisixnpm/planka-mcp"]
|
|
42
|
+
env = { PLANKA_BASE_URL = "http://localhost:3000", PLANKA_API_KEY = "your-api-key" }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### opencode (`opencode.json`)
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcp": {
|
|
50
|
+
"planka": {
|
|
51
|
+
"type": "local",
|
|
52
|
+
"command": ["npx", "-y", "@denisixnpm/planka-mcp"],
|
|
53
|
+
"environment": { "PLANKA_BASE_URL": "http://localhost:3000", "PLANKA_API_KEY": "your-api-key" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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`.
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
| Variable | Default | Description |
|
|
64
|
+
|----------|---------|-------------|
|
|
65
|
+
| `PLANKA_BASE_URL` | `http://localhost:3000` | Planka instance URL |
|
|
66
|
+
| `PLANKA_API_KEY` | — | API key auth (recommended; wins if both are set) |
|
|
67
|
+
| `PLANKA_USERNAME` / `PLANKA_PASSWORD` | — | Login auth (alternative to API key) |
|
|
68
|
+
| `PLANKA_HTTP_TIMEOUT_MS` | `30000` | Per-request timeout (aborts, then retries) |
|
|
69
|
+
| `PLANKA_HTTP_MAX_RETRIES` | `2` | Retries for `408`/`429`/`5xx` and network errors |
|
|
70
|
+
| `PLANKA_HTTP_RETRY_BASE_DELAY_MS` | `250` | Exponential backoff base (`base * 2^attempt`) |
|
|
71
|
+
| `PLANKA_CONDENSED_OUTPUT` | `true` | Condensed agent output; `false` = raw payloads |
|
|
72
|
+
| `ENABLE_ALL_TOOLS` | `false` | Enable all 28 tools (otherwise 11 core) |
|
|
73
|
+
| `ENABLE_ADMIN_TOOLS` / `ENABLE_OPTIONAL_TOOLS` | `false` | Enable one extra category |
|
|
74
|
+
| `MCP_TRANSPORT` | `stdio` | `stdio` (single client) or `sse` (multi-client) |
|
|
75
|
+
| `MCP_PORT` / `MCP_HOST` | `3001` / `127.0.0.1` | SSE mode bind; `0.0.0.0` in Docker |
|
|
76
|
+
| `MCP_AUTH_TOKEN` | — | If set, SSE endpoints require `Authorization: Bearer <token>` |
|
|
77
|
+
| `MCP_CORS_ORIGIN` | — | If set, emitted as `Access-Control-Allow-Origin` (no CORS by default) |
|
|
78
|
+
| `MCP_HEARTBEAT_INTERVAL_MS` | `30000` | SSE keep-alive interval for strict proxies |
|
|
79
|
+
|
|
80
|
+
Note: Bun auto-loads `.env` from the working directory (real env vars win); the Node/npx path does not.
|
|
81
|
+
|
|
82
|
+
## Agent workflow
|
|
83
|
+
|
|
84
|
+
**Scoped context** — select a project/board/list/card once, then omit ids:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{"name": "context", "arguments": {"action": "set", "data": {"boardId": "..."}}}
|
|
88
|
+
{"name": "cards", "arguments": {"action": "create", "data": {"name": "Ship it", "type": "project", "position": 1}}}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`context.set` returns an overview of what you opened (boards of the project, lists/cards of the board). Setting a higher level resets deeper ones. Scope is per SSE session; process-wide for stdio.
|
|
92
|
+
|
|
93
|
+
**Condensed output** — responses keep only agent-relevant fields (ids, names, positions, states); timestamps, password hashes and audit metadata are stripped, long text truncated, `included` sidecars reduced per entity. Escape hatches: per-call `raw: true` or `PLANKA_CONDENSED_OUTPUT=false`.
|
|
94
|
+
|
|
95
|
+
## Tools (28)
|
|
96
|
+
|
|
97
|
+
Every tool takes `action`, optional `id`/`data`/`query`, and optional `raw: true`.
|
|
98
|
+
|
|
99
|
+
| Category | Tools |
|
|
100
|
+
|----------|-------|
|
|
101
|
+
| **Core** (11, always on) | `auth`, `bootstrap`, `context`, `projects`, `boards`, `lists`, `cards`, `comments`, `tasks`, `labels`, `cardMembers` |
|
|
102
|
+
| **Admin** (4) | `config`, `users`, `webhooks`, `projectManagers` |
|
|
103
|
+
| **Optional** (13) | `attachments`, `backgroundImages`, `boardMembers`, `cardExtras`, `cardMemberExtras`, `commentExtras`, `customFields`, `labelExtras`, `listExtras`, `notifications`, `taskExtras`, `userInfo`, `actions` |
|
|
104
|
+
|
|
105
|
+
Actions per tool are enumerated in the MCP `tools/list` response. Live-verified semantics worth knowing: project managers exist only on `shared` projects; notification services are self-only; Planka v2 JSON link attachments HTTP 500 (multipart required); v1.26.2 has no REST comments/task-lists/custom-fields/webhooks (clean 404s), and renamed v1 membership/label routes are retried automatically.
|
|
106
|
+
|
|
107
|
+
## Multi-client SSE mode (optional)
|
|
108
|
+
|
|
109
|
+
To serve several clients over HTTP instead of stdio, run the same package in SSE mode:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
MCP_TRANSPORT=sse MCP_HOST=127.0.0.1 MCP_PORT=3001 MCP_AUTH_TOKEN=change-me \
|
|
113
|
+
PLANKA_BASE_URL=http://localhost:3000 PLANKA_API_KEY=your-api-key \
|
|
114
|
+
npx -y @denisixnpm/planka-mcp
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Connect MCP clients to `http://127.0.0.1:3001/sse`. Set `MCP_AUTH_TOKEN` whenever binding beyond
|
|
118
|
+
loopback (`MCP_HOST=0.0.0.0`) — without it, anyone who can reach the port has full tool access.
|
|
119
|
+
A `--healthcheck` mode is built in for orchestrators.
|
|
120
|
+
|
|
121
|
+
Prefer a container? The image is published on Docker Hub:
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
docker run -d -p 3001:3001 \
|
|
126
|
+
-e MCP_TRANSPORT=sse -e MCP_HOST=0.0.0.0 -e MCP_AUTH_TOKEN=change-me \
|
|
127
|
+
-e PLANKA_BASE_URL=http://host.docker.internal:3000 \
|
|
128
|
+
-e PLANKA_API_KEY=your-api-key \
|
|
129
|
+
denisix/planka-mcp:latest
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Compiled Bun binary on distroless (171 MB, no shell, no node_modules).
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bun install && bun test # build + full hermetic suite
|
|
138
|
+
bun run dev # run from source (stdio)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
End-to-end tests run against real Planka stacks (`docker compose --profile e2e up -d`, then `E2E_PLANKA_URL=... bun test`). See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
142
|
+
|
|
143
|
+
## Credits
|
|
144
|
+
|
|
145
|
+
Fork of [planka-mcp](https://github.com/chmald/planka-mcp) by [Christopher Maldonado](https://github.com/chmald) — credit for the original server goes to him. Maintained by [denisix](https://github.com/denisix), published as [`@denisixnpm/planka-mcp`](https://www.npmjs.com/package/@denisixnpm/planka-mcp).
|
|
146
|
+
|
|
147
|
+
**What this fork improves**
|
|
148
|
+
|
|
149
|
+
- **Security**: 0 `npm audit` findings; loopback-only SSE by default, timing-safe bearer auth, no wildcard CORS, validated login responses (no `Bearer undefined`), single-flight login, per-request timeouts.
|
|
150
|
+
- **Planka v1 compatibility**: renamed v1 routes retried transparently on 404 — one build serves v2 and v1.
|
|
151
|
+
- **Agent-optimized**: condensed output + `context` tool for scoped, id-free workflows.
|
|
152
|
+
- **Lean**: one production dependency (MCP SDK), built-in `fetch`, compiled distroless image.
|
|
153
|
+
- **Verified**: 117 tests incl. live E2E against real Planka 2.0.3 and 1.26.2, SSE protocol round-trips, engine edge cases.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT — see [LICENSE](LICENSE). Issues: [github.com/denisix/planka-mcp/issues](https://github.com/denisix/planka-mcp/issues).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response condenser: shrinks Planka API payloads to the fields an AI agent
|
|
3
|
+
* (Claude Code, Codex, opencode) actually needs, protecting the client's
|
|
4
|
+
* context window. Planka envelopes every response as {item|items, included}
|
|
5
|
+
* with full audit metadata on each entity; the condenser keeps the structure
|
|
6
|
+
* but reduces every entity to its whitelist and truncates long text.
|
|
7
|
+
*
|
|
8
|
+
* Semantics:
|
|
9
|
+
* - {items, included} -> {items: [...reduced], included: {...reduced}}
|
|
10
|
+
* - {item, included} -> {item: reduced, included: {...reduced}}
|
|
11
|
+
* - bare arrays -> each entity reduced (defensive; mocks, older shapes)
|
|
12
|
+
* - whitelists -> per-tool field lists; unknown tools fall back to a
|
|
13
|
+
* generic id/name/... pick of primitive values
|
|
14
|
+
* - strings -> truncated to TEXT_LIMIT characters
|
|
15
|
+
* - auth/context -> verbatim (tokens and local state must not be mangled)
|
|
16
|
+
*/
|
|
17
|
+
/** Reduce one tool result (envelope, list, or raw value). */
|
|
18
|
+
export declare function condenseResult(toolName: string, data: unknown): unknown;
|
package/dist/condense.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response condenser: shrinks Planka API payloads to the fields an AI agent
|
|
3
|
+
* (Claude Code, Codex, opencode) actually needs, protecting the client's
|
|
4
|
+
* context window. Planka envelopes every response as {item|items, included}
|
|
5
|
+
* with full audit metadata on each entity; the condenser keeps the structure
|
|
6
|
+
* but reduces every entity to its whitelist and truncates long text.
|
|
7
|
+
*
|
|
8
|
+
* Semantics:
|
|
9
|
+
* - {items, included} -> {items: [...reduced], included: {...reduced}}
|
|
10
|
+
* - {item, included} -> {item: reduced, included: {...reduced}}
|
|
11
|
+
* - bare arrays -> each entity reduced (defensive; mocks, older shapes)
|
|
12
|
+
* - whitelists -> per-tool field lists; unknown tools fall back to a
|
|
13
|
+
* generic id/name/... pick of primitive values
|
|
14
|
+
* - strings -> truncated to TEXT_LIMIT characters
|
|
15
|
+
* - auth/context -> verbatim (tokens and local state must not be mangled)
|
|
16
|
+
*/
|
|
17
|
+
const TEXT_LIMIT = 300;
|
|
18
|
+
/** Entity-specific field whitelists, keyed by tool name. */
|
|
19
|
+
const ENTITY_FIELDS = {
|
|
20
|
+
projects: ["id", "name", "type"],
|
|
21
|
+
boards: ["id", "name", "position"],
|
|
22
|
+
lists: ["id", "name", "type", "position"],
|
|
23
|
+
listExtras: ["id", "name", "type", "position"],
|
|
24
|
+
cards: ["id", "name", "description", "position", "listId", "isArchived"],
|
|
25
|
+
cardExtras: ["id", "name", "description", "position", "listId", "isArchived"],
|
|
26
|
+
tasks: ["id", "name", "isCompleted", "taskListId"],
|
|
27
|
+
taskExtras: ["id", "name", "isCompleted", "taskListId"],
|
|
28
|
+
taskLists: ["id", "name", "position", "cardId"],
|
|
29
|
+
labels: ["id", "name", "color", "position"],
|
|
30
|
+
labelExtras: ["id", "cardId", "labelId"],
|
|
31
|
+
comments: ["id", "text", "cardId"],
|
|
32
|
+
commentExtras: ["id", "text", "cardId"],
|
|
33
|
+
attachments: ["id", "name", "url", "type", "cardId"],
|
|
34
|
+
users: ["id", "email", "name", "username", "role"],
|
|
35
|
+
userInfo: ["id", "email", "name", "username", "role"],
|
|
36
|
+
boardMembers: ["id", "userId", "boardId", "role"],
|
|
37
|
+
cardMembers: ["id", "userId", "cardId"],
|
|
38
|
+
cardMemberExtras: ["id", "userId", "cardId"],
|
|
39
|
+
projectManagers: ["id", "userId", "projectId"],
|
|
40
|
+
customFields: ["id", "name", "position", "content", "value"],
|
|
41
|
+
customFieldGroups: ["id", "name", "position"],
|
|
42
|
+
customFieldValues: ["id", "cardId", "customFieldId", "content"],
|
|
43
|
+
baseCustomFieldGroups: ["id", "name", "position"],
|
|
44
|
+
webhooks: ["id", "name", "url"],
|
|
45
|
+
backgroundImages: ["id", "name", "boardId"],
|
|
46
|
+
notifications: ["id", "type", "isRead", "cardId"],
|
|
47
|
+
actions: ["id", "type", "cardId", "userId"],
|
|
48
|
+
};
|
|
49
|
+
/** `included` keys that do not match their tool name verbatim. */
|
|
50
|
+
const INCLUDED_KEY_TO_TOOL = {
|
|
51
|
+
boardMemberships: "boardMembers",
|
|
52
|
+
cardMemberships: "cardMembers",
|
|
53
|
+
cardLabels: "labelExtras",
|
|
54
|
+
taskLists: "taskLists",
|
|
55
|
+
customFieldGroups: "customFieldGroups",
|
|
56
|
+
customFieldValues: "customFieldValues",
|
|
57
|
+
baseCustomFieldGroups: "baseCustomFieldGroups",
|
|
58
|
+
};
|
|
59
|
+
/** Generic whitelist used when a tool has no specific mapping. */
|
|
60
|
+
const GENERIC_FIELDS = [
|
|
61
|
+
"id", "name", "email", "username", "url", "text", "type", "position",
|
|
62
|
+
"color", "role", "isCompleted", "isRead", "cardId", "listId", "boardId",
|
|
63
|
+
"projectId", "userId",
|
|
64
|
+
];
|
|
65
|
+
/** Tools whose payloads are returned verbatim (tokens, local state). */
|
|
66
|
+
const PASSTHROUGH_TOOLS = { auth: true, context: true };
|
|
67
|
+
function truncateText(value) {
|
|
68
|
+
return value.length > TEXT_LIMIT ? value.slice(0, TEXT_LIMIT) + "…" : value;
|
|
69
|
+
}
|
|
70
|
+
function condenseEntity(toolName, entity) {
|
|
71
|
+
if (typeof entity !== "object" || entity === null || Array.isArray(entity)) {
|
|
72
|
+
return entity;
|
|
73
|
+
}
|
|
74
|
+
const source = entity;
|
|
75
|
+
const whitelist = ENTITY_FIELDS[toolName] ?? GENERIC_FIELDS;
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const field of whitelist) {
|
|
78
|
+
const value = source[field];
|
|
79
|
+
if (value !== null && value !== undefined) {
|
|
80
|
+
out[field] = typeof value === "string" ? truncateText(value) : value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return Object.keys(out).length > 0 ? out : entity;
|
|
84
|
+
}
|
|
85
|
+
/** Reduce every entity array inside an `included` sidecar object. */
|
|
86
|
+
function condenseIncluded(included) {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const [key, value] of Object.entries(included)) {
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
const toolName = INCLUDED_KEY_TO_TOOL[key] ?? key;
|
|
91
|
+
out[key] = value.map(entity => condenseEntity(toolName, entity));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Reduce one tool result (envelope, list, or raw value). */
|
|
97
|
+
export function condenseResult(toolName, data) {
|
|
98
|
+
if (PASSTHROUGH_TOOLS[toolName]) {
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(data)) {
|
|
102
|
+
return data.map(item => condenseEntity(toolName, item));
|
|
103
|
+
}
|
|
104
|
+
if (typeof data === "object" && data !== null) {
|
|
105
|
+
const source = data;
|
|
106
|
+
const hasItems = Array.isArray(source.items);
|
|
107
|
+
const hasItem = "item" in source;
|
|
108
|
+
if (hasItems || hasItem) {
|
|
109
|
+
const out = {};
|
|
110
|
+
if (hasItems) {
|
|
111
|
+
out.items = source.items.map(item => condenseEntity(toolName, item));
|
|
112
|
+
}
|
|
113
|
+
if (hasItem) {
|
|
114
|
+
out.item = condenseEntity(toolName, source.item);
|
|
115
|
+
}
|
|
116
|
+
if (source.included && typeof source.included === "object") {
|
|
117
|
+
const included = condenseIncluded(source.included);
|
|
118
|
+
if (Object.keys(included).length > 0) {
|
|
119
|
+
out.included = included;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
return condenseEntity(toolName, data);
|
|
125
|
+
}
|
|
126
|
+
return data;
|
|
127
|
+
}
|
package/dist/server.d.ts
ADDED