@omnicoreos/planka-mcp 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.
Files changed (126) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/CONTRIBUTING.md +37 -0
  3. package/CREDITS.md +39 -0
  4. package/LICENSE +22 -0
  5. package/README.es.md +213 -0
  6. package/README.md +213 -0
  7. package/dist/client.d.ts +96 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +281 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +85 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +226 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/identity.generated.d.ts +3 -0
  16. package/dist/identity.generated.d.ts.map +1 -0
  17. package/dist/identity.generated.js +4 -0
  18. package/dist/identity.generated.js.map +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +93 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/operations/attachments.d.ts +34 -0
  24. package/dist/operations/attachments.d.ts.map +1 -0
  25. package/dist/operations/attachments.js +89 -0
  26. package/dist/operations/attachments.js.map +1 -0
  27. package/dist/operations/board-id.d.ts +9 -0
  28. package/dist/operations/board-id.d.ts.map +1 -0
  29. package/dist/operations/board-id.js +86 -0
  30. package/dist/operations/board-id.js.map +1 -0
  31. package/dist/operations/boards.d.ts +143 -0
  32. package/dist/operations/boards.d.ts.map +1 -0
  33. package/dist/operations/boards.js +391 -0
  34. package/dist/operations/boards.js.map +1 -0
  35. package/dist/operations/cards.d.ts +36 -0
  36. package/dist/operations/cards.d.ts.map +1 -0
  37. package/dist/operations/cards.js +81 -0
  38. package/dist/operations/cards.js.map +1 -0
  39. package/dist/operations/comments.d.ts +28 -0
  40. package/dist/operations/comments.d.ts.map +1 -0
  41. package/dist/operations/comments.js +53 -0
  42. package/dist/operations/comments.js.map +1 -0
  43. package/dist/operations/labels.d.ts +60 -0
  44. package/dist/operations/labels.d.ts.map +1 -0
  45. package/dist/operations/labels.js +146 -0
  46. package/dist/operations/labels.js.map +1 -0
  47. package/dist/operations/lists.d.ts +15 -0
  48. package/dist/operations/lists.d.ts.map +1 -0
  49. package/dist/operations/lists.js +40 -0
  50. package/dist/operations/lists.js.map +1 -0
  51. package/dist/operations/projects.d.ts +24 -0
  52. package/dist/operations/projects.d.ts.map +1 -0
  53. package/dist/operations/projects.js +61 -0
  54. package/dist/operations/projects.js.map +1 -0
  55. package/dist/operations/tasks.d.ts +30 -0
  56. package/dist/operations/tasks.d.ts.map +1 -0
  57. package/dist/operations/tasks.js +111 -0
  58. package/dist/operations/tasks.js.map +1 -0
  59. package/dist/schemas/entities.d.ts +389 -0
  60. package/dist/schemas/entities.d.ts.map +1 -0
  61. package/dist/schemas/entities.js +202 -0
  62. package/dist/schemas/entities.js.map +1 -0
  63. package/dist/schemas/requests.d.ts +539 -0
  64. package/dist/schemas/requests.d.ts.map +1 -0
  65. package/dist/schemas/requests.js +187 -0
  66. package/dist/schemas/requests.js.map +1 -0
  67. package/dist/schemas/responses.d.ts +2626 -0
  68. package/dist/schemas/responses.d.ts.map +1 -0
  69. package/dist/schemas/responses.js +75 -0
  70. package/dist/schemas/responses.js.map +1 -0
  71. package/dist/tools/attachments.d.ts +270 -0
  72. package/dist/tools/attachments.d.ts.map +1 -0
  73. package/dist/tools/attachments.js +247 -0
  74. package/dist/tools/attachments.js.map +1 -0
  75. package/dist/tools/cards.d.ts +401 -0
  76. package/dist/tools/cards.d.ts.map +1 -0
  77. package/dist/tools/cards.js +368 -0
  78. package/dist/tools/cards.js.map +1 -0
  79. package/dist/tools/comments.d.ts +217 -0
  80. package/dist/tools/comments.d.ts.map +1 -0
  81. package/dist/tools/comments.js +208 -0
  82. package/dist/tools/comments.js.map +1 -0
  83. package/dist/tools/index.d.ts +1293 -0
  84. package/dist/tools/index.d.ts.map +1 -0
  85. package/dist/tools/index.js +62 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/tools/labels.d.ts +214 -0
  88. package/dist/tools/labels.d.ts.map +1 -0
  89. package/dist/tools/labels.js +302 -0
  90. package/dist/tools/labels.js.map +1 -0
  91. package/dist/tools/lists.d.ts +117 -0
  92. package/dist/tools/lists.d.ts.map +1 -0
  93. package/dist/tools/lists.js +178 -0
  94. package/dist/tools/lists.js.map +1 -0
  95. package/dist/tools/navigation.d.ts +106 -0
  96. package/dist/tools/navigation.d.ts.map +1 -0
  97. package/dist/tools/navigation.js +151 -0
  98. package/dist/tools/navigation.js.map +1 -0
  99. package/dist/tools/queries.d.ts +380 -0
  100. package/dist/tools/queries.d.ts.map +1 -0
  101. package/dist/tools/queries.js +256 -0
  102. package/dist/tools/queries.js.map +1 -0
  103. package/dist/tools/tasks.d.ts +223 -0
  104. package/dist/tools/tasks.d.ts.map +1 -0
  105. package/dist/tools/tasks.js +169 -0
  106. package/dist/tools/tasks.js.map +1 -0
  107. package/docs/planka-2x-gotchas.md +192 -0
  108. package/docs/tools.md +659 -0
  109. package/docs/troubleshooting.md +138 -0
  110. package/package.json +71 -0
  111. package/project.identity.json +7 -0
  112. package/scripts/bootstrap-board.mjs +160 -0
  113. package/scripts/lib/planka-api.mjs +244 -0
  114. package/scripts/lib/workflow-template.mjs +26 -0
  115. package/scripts/setup.mjs +236 -0
  116. package/scripts/setup.sh +48 -0
  117. package/scripts/sync-identity.mjs +57 -0
  118. package/server.json +43 -0
  119. package/tests/smoke/planka-smoke.mjs +920 -0
  120. package/workflow/README.md +232 -0
  121. package/workflow/board-template.md +115 -0
  122. package/workflow/skills/planka-close-card/SKILL.md +81 -0
  123. package/workflow/skills/planka-orchestrator/SKILL.md +126 -0
  124. package/workflow/worktrees/README.md +104 -0
  125. package/workflow/worktrees/wt.conf.example +24 -0
  126. package/workflow/worktrees/wt.sh +345 -0
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { constants } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { createInterface } from "node:readline/promises";
10
+ import { stdin as input, stdout as output } from "node:process";
11
+ import { bootstrapBoard } from "./bootstrap-board.mjs";
12
+ import { PlankaApi, PlankaApiError, boardEntities } from "./lib/planka-api.mjs";
13
+
14
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
15
+ const serverEntry = resolve(root, "dist/index.js");
16
+ const credentials = {
17
+ baseUrl: process.env.PLANKA_BASE_URL,
18
+ email: process.env.PLANKA_AGENT_EMAIL,
19
+ password: process.env.PLANKA_AGENT_PASSWORD,
20
+ };
21
+ const rl = createInterface({ input, output });
22
+
23
+ async function ask(question, fallback = "") {
24
+ const suffix = fallback ? ` [${fallback}]` : "";
25
+ const answer = (await rl.question(`${question}${suffix}: `)).trim();
26
+ return answer || fallback;
27
+ }
28
+
29
+ async function choose(question, items) {
30
+ console.log(`\n${question}`);
31
+ items.forEach((item, index) => console.log(` ${index + 1}) ${item.label}`));
32
+ while (true) {
33
+ const raw = await ask("Choose", "1");
34
+ const index = Number(raw) - 1;
35
+ if (Number.isInteger(index) && items[index]) return items[index].value;
36
+ console.log(`Enter a number from 1 to ${items.length}.`);
37
+ }
38
+ }
39
+
40
+ async function writePrivateLauncher() {
41
+ const configDir = resolve(
42
+ process.env.XDG_CONFIG_HOME || resolve(homedir(), ".config"),
43
+ "planka-mcp"
44
+ );
45
+ const configPath = resolve(configDir, "config.json");
46
+ const binDir = resolve(homedir(), ".local", "bin");
47
+ const launcherPath = resolve(binDir, "planka-mcp");
48
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
49
+ await mkdir(binDir, { recursive: true, mode: 0o755 });
50
+ await writeFile(
51
+ configPath,
52
+ `${JSON.stringify(
53
+ {
54
+ baseUrl: credentials.baseUrl.replace(/\/+$/, ""),
55
+ email: credentials.email,
56
+ password: credentials.password,
57
+ serverEntry,
58
+ },
59
+ null,
60
+ 2
61
+ )}\n`,
62
+ { mode: 0o600 }
63
+ );
64
+ await chmod(configPath, 0o600);
65
+
66
+ const launcher = `#!/usr/bin/env node
67
+ import { readFile } from "node:fs/promises";
68
+ import { pathToFileURL } from "node:url";
69
+ const config = JSON.parse(await readFile(${JSON.stringify(configPath)}, "utf8"));
70
+ process.env.PLANKA_BASE_URL = config.baseUrl;
71
+ process.env.PLANKA_AGENT_EMAIL = config.email;
72
+ process.env.PLANKA_AGENT_PASSWORD = config.password;
73
+ await import(pathToFileURL(config.serverEntry).href);
74
+ `;
75
+ await writeFile(launcherPath, launcher, { mode: 0o700 });
76
+ await chmod(launcherPath, 0o700);
77
+ return { configPath, launcherPath };
78
+ }
79
+
80
+ async function configureClaude(launcherPath) {
81
+ console.log("\nChoose how Claude Code should load the server:");
82
+ console.log(" CLI/user scope: private to this machine and available in every project.");
83
+ console.log(" .mcp.json: committed with a project so every teammate gets the same server entry; credentials stay outside Git.");
84
+ const mode = await choose("Configuration method", [
85
+ { label: "claude mcp add (easiest)", value: "cli" },
86
+ { label: ".mcp.json in a project (team-friendly)", value: "project" },
87
+ ]);
88
+
89
+ if (mode === "cli") {
90
+ const probe = spawnSync("claude", ["--version"], { encoding: "utf8" });
91
+ if (probe.error?.code === "ENOENT") {
92
+ throw new Error(
93
+ "Claude Code is not installed or is not on PATH. Install it, then rerun setup."
94
+ );
95
+ }
96
+ spawnSync("claude", ["mcp", "remove", "planka", "--scope", "user"], {
97
+ stdio: "ignore",
98
+ });
99
+ const added = spawnSync(
100
+ "claude",
101
+ [
102
+ "mcp",
103
+ "add",
104
+ "--scope",
105
+ "user",
106
+ "--transport",
107
+ "stdio",
108
+ "planka",
109
+ "--",
110
+ launcherPath,
111
+ ],
112
+ { encoding: "utf8" }
113
+ );
114
+ if (added.status !== 0) {
115
+ throw new Error(
116
+ `claude mcp add failed: ${(added.stderr || added.stdout || "unknown error").trim()}`
117
+ );
118
+ }
119
+ console.log("Claude Code user configuration updated.");
120
+ return;
121
+ }
122
+
123
+ const projectDir = resolve(await ask("Project directory", process.cwd()));
124
+ await access(projectDir, constants.R_OK | constants.W_OK);
125
+ const mcpPath = resolve(projectDir, ".mcp.json");
126
+ let document = { mcpServers: {} };
127
+ try {
128
+ document = JSON.parse(await readFile(mcpPath, "utf8"));
129
+ } catch (error) {
130
+ if (error.code !== "ENOENT") {
131
+ throw new Error(`Cannot parse ${mcpPath}: ${error.message}`);
132
+ }
133
+ }
134
+ if (!document.mcpServers || typeof document.mcpServers !== "object") {
135
+ document.mcpServers = {};
136
+ }
137
+ document.mcpServers.planka = {
138
+ type: "stdio",
139
+ command: "${HOME}/.local/bin/planka-mcp",
140
+ args: [],
141
+ };
142
+ await writeFile(mcpPath, `${JSON.stringify(document, null, 2)}\n`);
143
+ console.log(`Wrote ${mcpPath}`);
144
+ console.log("Claude Code will ask you to trust this project-scoped server on first use.");
145
+ }
146
+
147
+ async function selectBoard(api) {
148
+ const projectsResponse = await api.getProjects();
149
+ const projects = Array.isArray(projectsResponse?.items)
150
+ ? projectsResponse.items
151
+ : [];
152
+ const boards = Array.isArray(projectsResponse?.included?.boards)
153
+ ? projectsResponse.included.boards
154
+ : [];
155
+ if (!projects.length) {
156
+ throw new Error("The agent user cannot see any Planka projects.");
157
+ }
158
+
159
+ const boardOptions = boards.map((board) => {
160
+ const project = projects.find((item) => item.id === board.projectId);
161
+ return {
162
+ label: `${project?.name || "Unknown project"} / ${board.name}`,
163
+ value: { kind: "existing", board },
164
+ };
165
+ });
166
+ boardOptions.push({
167
+ label: "Create a new board with the optional workflow template",
168
+ value: { kind: "create" },
169
+ });
170
+ const selection = await choose("Boards visible to the agent user", boardOptions);
171
+
172
+ if (selection.kind === "create") {
173
+ const projectId = await choose(
174
+ "Project for the new board (the agent user must be a project manager)",
175
+ projects.map((project) => ({ label: project.name, value: project.id }))
176
+ );
177
+ const boardName = await ask("New board name", "Agent Work");
178
+ const result = await bootstrapBoard(api, { projectId, boardName });
179
+ console.log(
180
+ `Created ${result.boardName} with ${result.listCount} lists and ${result.labelCount} labels.`
181
+ );
182
+ return result.boardId;
183
+ }
184
+
185
+ const boardId = selection.board.id;
186
+ const state = boardEntities(await api.getBoard(boardId));
187
+ const usableLists = state.lists.filter((item) => item.name && item.type !== "closed");
188
+ if (!usableLists.length) {
189
+ console.log("The selected board has no active list, so setup will install the workflow template.");
190
+ await bootstrapBoard(api, { boardId });
191
+ }
192
+ return boardId;
193
+ }
194
+
195
+ async function runSmoke(boardId) {
196
+ console.log("\nRunning the real MCP smoke test (the temporary card is deleted)...");
197
+ const result = spawnSync(
198
+ process.execPath,
199
+ [resolve(root, "tests/smoke/planka-smoke.mjs")],
200
+ {
201
+ cwd: root,
202
+ stdio: "inherit",
203
+ env: {
204
+ ...process.env,
205
+ PLANKA_SMOKE_BOARD_ID: boardId,
206
+ PLANKA_MCP_ENTRY: serverEntry,
207
+ },
208
+ }
209
+ );
210
+ if (result.status !== 0) {
211
+ throw new Error("The real smoke test failed. The MCP configuration was written, but setup is not verified.");
212
+ }
213
+ }
214
+
215
+ async function main() {
216
+ try {
217
+ console.log("\nValidating credentials against POST /api/access-tokens...");
218
+ const api = await PlankaApi.fromCredentials(credentials);
219
+ console.log("Credentials valid. JSON API response received.");
220
+ const boardId = await selectBoard(api);
221
+ const { launcherPath } = await writePrivateLauncher();
222
+ await configureClaude(launcherPath);
223
+ await runSmoke(boardId);
224
+ console.log("\nSetup complete.");
225
+ console.log('Reiniciá Claude Code y pedile “mostrame mi board de Planka”.');
226
+ console.log('Restart Claude Code, approve the server if prompted, and ask: “show me my Planka board”.');
227
+ } finally {
228
+ rl.close();
229
+ }
230
+ }
231
+
232
+ main().catch((error) => {
233
+ const prefix = error instanceof PlankaApiError ? "Planka API error" : "Setup error";
234
+ console.error(`\n${prefix}: ${error.message}`);
235
+ process.exit(1);
236
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+
7
+ if ! command -v node >/dev/null 2>&1; then
8
+ echo "setup: Node.js 18 or newer is required." >&2
9
+ exit 1
10
+ fi
11
+
12
+ NODE_MAJOR="$(node -p 'Number(process.versions.node.split(".")[0])')"
13
+ if [[ "$NODE_MAJOR" -lt 18 ]]; then
14
+ echo "setup: Node.js 18 or newer is required (found $(node --version))." >&2
15
+ exit 1
16
+ fi
17
+
18
+ echo "Planka MCP setup"
19
+ echo
20
+
21
+ DEFAULT_URL="${PLANKA_BASE_URL:-https://planka.example.com}"
22
+ read -r -p "Planka base URL [$DEFAULT_URL]: " INPUT_URL
23
+ export PLANKA_BASE_URL="${INPUT_URL:-$DEFAULT_URL}"
24
+
25
+ DEFAULT_EMAIL="${PLANKA_AGENT_EMAIL:-}"
26
+ if [[ -n "$DEFAULT_EMAIL" ]]; then
27
+ read -r -p "Agent email or username [$DEFAULT_EMAIL]: " INPUT_EMAIL
28
+ export PLANKA_AGENT_EMAIL="${INPUT_EMAIL:-$DEFAULT_EMAIL}"
29
+ else
30
+ read -r -p "Agent email or username: " PLANKA_AGENT_EMAIL
31
+ export PLANKA_AGENT_EMAIL
32
+ fi
33
+
34
+ if [[ -z "${PLANKA_AGENT_PASSWORD:-}" ]]; then
35
+ read -r -s -p "Agent password: " PLANKA_AGENT_PASSWORD
36
+ echo
37
+ export PLANKA_AGENT_PASSWORD
38
+ else
39
+ echo "Using PLANKA_AGENT_PASSWORD from the environment."
40
+ fi
41
+
42
+ if [[ ! -f "$ROOT_DIR/dist/index.js" ]]; then
43
+ echo
44
+ echo "Building the MCP server..."
45
+ (cd "$ROOT_DIR" && npm ci && npm run build)
46
+ fi
47
+
48
+ exec node "$ROOT_DIR/scripts/setup.mjs"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const checkOnly = process.argv.includes("--check");
9
+ const readJson = async (name) =>
10
+ JSON.parse(await readFile(resolve(root, name), "utf8"));
11
+
12
+ const identity = await readJson("project.identity.json");
13
+ const packageJson = await readJson("package.json");
14
+ const serverJson = await readJson("server.json");
15
+
16
+ packageJson.name = identity.packageName;
17
+ packageJson.mcpName = identity.mcpServerName;
18
+ packageJson.bin = { [identity.binName]: "dist/index.js" };
19
+ packageJson.repository.url = `${identity.repositoryUrl}.git`;
20
+ packageJson.bugs.url = `${identity.repositoryUrl}/issues`;
21
+ packageJson.homepage = `${identity.repositoryUrl}#readme`;
22
+
23
+ serverJson.name = identity.mcpServerName;
24
+ serverJson.repository.url = identity.repositoryUrl;
25
+ serverJson.version = packageJson.version;
26
+ serverJson.packages[0].identifier = identity.packageName;
27
+ serverJson.packages[0].version = packageJson.version;
28
+
29
+ const generatedIdentity = `// Generated by scripts/sync-identity.mjs. Edit project.identity.json instead.\nexport const SERVER_NAME = ${JSON.stringify(identity.mcpServerName)};\nexport const SERVER_VERSION = ${JSON.stringify(packageJson.version)};\n`;
30
+
31
+ const outputs = new Map([
32
+ ["package.json", `${JSON.stringify(packageJson, null, 2)}\n`],
33
+ ["server.json", `${JSON.stringify(serverJson, null, 2)}\n`],
34
+ ["src/identity.generated.ts", generatedIdentity],
35
+ ]);
36
+
37
+ let stale = false;
38
+ for (const [name, expected] of outputs) {
39
+ const path = resolve(root, name);
40
+ let current = "";
41
+ try {
42
+ current = await readFile(path, "utf8");
43
+ } catch {
44
+ // A missing generated file is stale and will be created below.
45
+ }
46
+
47
+ if (current === expected) continue;
48
+ stale = true;
49
+ if (!checkOnly) await writeFile(path, expected);
50
+ }
51
+
52
+ if (checkOnly && stale) {
53
+ console.error("Identity files are stale. Run: npm run sync:identity");
54
+ process.exit(1);
55
+ }
56
+
57
+ if (!checkOnly) console.log("Identity files synchronized.");
package/server.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.omnicoreos/planka-mcp",
4
+ "description": "Planka 2.x MCP server with verified card, task, comment, label, and list operations.",
5
+ "repository": {
6
+ "url": "https://github.com/omnicoreos/planka-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "0.2.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@omnicoreos/planka-mcp",
14
+ "version": "0.2.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "name": "PLANKA_BASE_URL",
21
+ "description": "Planka base URL, for example https://planka.example.com",
22
+ "isRequired": true,
23
+ "format": "string",
24
+ "isSecret": false
25
+ },
26
+ {
27
+ "name": "PLANKA_AGENT_EMAIL",
28
+ "description": "Email address or username for the Planka agent user",
29
+ "isRequired": true,
30
+ "format": "string",
31
+ "isSecret": false
32
+ },
33
+ {
34
+ "name": "PLANKA_AGENT_PASSWORD",
35
+ "description": "Password for the Planka agent user",
36
+ "isRequired": true,
37
+ "format": "string",
38
+ "isSecret": true
39
+ }
40
+ ]
41
+ }
42
+ ]
43
+ }