@kaneo/mcp 0.1.5 → 0.1.7

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
@@ -1,9 +1,11 @@
1
1
  # Kaneo MCP server
2
2
 
3
- `@kaneo/mcp` is a local MCP server for Kaneo.
3
+ [`@kaneo/mcp`](https://www.npmjs.com/package/@kaneo/mcp) is the official MCP (Model Context Protocol) server for [Kaneo](https://kaneo.app), the open source project management platform. It is maintained in the [usekaneo/kaneo](https://github.com/usekaneo/kaneo) monorepo and published to npm by the Kaneo team.
4
4
 
5
5
  It runs over stdio, signs in with Kaneo's device flow, and then calls the Kaneo API with a bearer token. The package lives in `packages/mcp` in this monorepo and exposes the `kaneo-mcp` CLI.
6
6
 
7
+ > **Tip:** Every Kaneo instance also ships a built-in HTTP MCP endpoint at `/api/mcp`. If your MCP client supports Streamable HTTP transport (e.g. Claude Code), you can connect directly without this package. See the [MCP docs](https://docs.kaneo.app/core/integrations/mcp) for details.
8
+
7
9
  ## Prerequisites
8
10
 
9
11
  - Node.js 20+
@@ -23,6 +25,7 @@ DEVICE_AUTH_CLIENT_IDS=kaneo-cli,kaneo-mcp,your-client-id
23
25
  |----------|-------------|
24
26
  | `KANEO_API_URL` | Kaneo API origin (default `http://localhost:1337`). Do not include `/api`. |
25
27
  | `KANEO_MCP_CLIENT_ID` | Device-flow client id (default `kaneo-mcp`). Must match `DEVICE_AUTH_CLIENT_IDS` on the server. |
28
+ | `KANEO_API_KEY` | **Optional.** A Kaneo API key (create one under Settings → Account → Developer). When set, the server authenticates with it as a Bearer token and skips the interactive device flow. Use this for headless/Docker setups. |
26
29
 
27
30
  ## Install
28
31
 
@@ -106,10 +109,21 @@ On the first tool call that needs Kaneo, the server:
106
109
  4. Polls `POST /api/auth/device/token` until approved
107
110
  5. Stores the access token at `~/.config/kaneo-mcp/credentials.json` with mode `0600`
108
111
 
112
+ ### Non-interactive (API key)
113
+
114
+ For headless or sandboxed environments where opening a browser is impractical, set `KANEO_API_KEY` to a key created under Settings → Account → Developer. The server sends it as a Bearer token on every request and skips the device flow entirely, so no token is cached to disk.
115
+
109
116
  ## Tools
110
117
 
111
118
  - Session: `whoami`, `list_workspaces`
112
119
  - Projects: `list_projects`, `get_project`, `create_project`, `update_project`
113
120
  - Tasks: `list_tasks`, `get_task`, `create_task`, `update_task`, `move_task`, `update_task_status`
114
- - Comments: `list_task_comments`, `create_task_comment`
115
- - Labels: `list_workspace_labels`, `create_label`, `attach_label_to_task`, `detach_label_from_task`
121
+ - Comments: `list_task_comments`, `create_task_comment`, `update_task_comment`, `delete_task_comment`
122
+ - Labels: `list_workspace_labels`, `create_label`, `attach_label_to_task`, `detach_label_from_task`, `delete_label`
123
+ - Task relations: `create_task_relation`, `get_task_relations`, `delete_task_relation`
124
+
125
+ ## Releasing
126
+
127
+ Bump `version` in `packages/mcp/package.json` and merge to `main`. The [publish workflow](../../.github/workflows/publish-mcp.yml) runs the package tests, publishes the new version to npm, and creates a `mcp-v<version>` GitHub release. Nothing is published while the version stays the same, so tool changes reach npm only once the version is bumped.
128
+
129
+ Publishing a GitHub release manually also works: tag it `mcp-v<version>` with the tag version matching `packages/mcp/package.json` on the tagged commit.
@@ -0,0 +1,24 @@
1
+ export type AuthServiceOptions = {
2
+ baseUrl: string;
3
+ clientId: string;
4
+ apiKey?: string;
5
+ };
6
+ export declare class AuthService {
7
+ readonly baseUrl: string;
8
+ readonly clientId: string;
9
+ private readonly apiKey?;
10
+ private activeGetAccessTokenPromise?;
11
+ constructor(options: AuthServiceOptions);
12
+ /**
13
+ * True when a pre-created Kaneo API key is used instead of the interactive
14
+ * device flow. Callers use this to avoid clearing/retrying auth on a 401.
15
+ */
16
+ get usingApiKey(): boolean;
17
+ clearToken(): Promise<void>;
18
+ private validateAccessToken;
19
+ private log;
20
+ /**
21
+ * Returns a valid access token, running the device authorization flow if needed.
22
+ */
23
+ getAccessToken(): Promise<string>;
24
+ }
@@ -4,12 +4,24 @@ import { clearCredentials, loadCredentials, saveCredentials, } from "./token-sto
4
4
  export class AuthService {
5
5
  baseUrl;
6
6
  clientId;
7
+ apiKey;
7
8
  activeGetAccessTokenPromise;
8
9
  constructor(options) {
9
10
  this.baseUrl = options.baseUrl;
10
11
  this.clientId = options.clientId;
12
+ this.apiKey = options.apiKey;
13
+ }
14
+ /**
15
+ * True when a pre-created Kaneo API key is used instead of the interactive
16
+ * device flow. Callers use this to avoid clearing/retrying auth on a 401.
17
+ */
18
+ get usingApiKey() {
19
+ return Boolean(this.apiKey);
11
20
  }
12
21
  async clearToken() {
22
+ // Nothing to clear for API-key auth; the key is static config.
23
+ if (this.apiKey)
24
+ return;
13
25
  await clearCredentials();
14
26
  }
15
27
  async validateAccessToken(token) {
@@ -39,6 +51,12 @@ export class AuthService {
39
51
  * Returns a valid access token, running the device authorization flow if needed.
40
52
  */
41
53
  async getAccessToken() {
54
+ // Non-interactive auth: a pre-created Kaneo API key (KANEO_API_KEY) is sent
55
+ // as a Bearer token, which the REST API already accepts. This skips the
56
+ // device flow so the MCP server works in headless/Docker environments.
57
+ if (this.apiKey) {
58
+ return this.apiKey;
59
+ }
42
60
  if (this.activeGetAccessTokenPromise) {
43
61
  return await this.activeGetAccessTokenPromise;
44
62
  }
@@ -0,0 +1,21 @@
1
+ export type DeviceCodeResponse = {
2
+ device_code: string;
3
+ user_code: string;
4
+ verification_uri: string;
5
+ verification_uri_complete?: string;
6
+ interval: number;
7
+ expires_in: number;
8
+ };
9
+ export type DeviceTokenErrorBody = {
10
+ error?: string;
11
+ error_description?: string;
12
+ };
13
+ export declare function requestDeviceCode(baseUrl: string, clientId: string): Promise<DeviceCodeResponse>;
14
+ /**
15
+ * Polls `/api/auth/device/token` until success or terminal error.
16
+ * First attempt is immediate; subsequent attempts wait `interval` seconds (increased on `slow_down`).
17
+ */
18
+ export declare function pollDeviceAccessToken(baseUrl: string, clientId: string, deviceCode: string, initialIntervalSec: number, options?: {
19
+ maxWaitMs?: number;
20
+ log?: (msg: string) => void;
21
+ }): Promise<string>;
@@ -0,0 +1,10 @@
1
+ export type StoredCredentials = {
2
+ version: 1;
3
+ baseUrl: string;
4
+ clientId: string;
5
+ accessToken: string;
6
+ };
7
+ export declare function credentialsPath(): string;
8
+ export declare function loadCredentials(): Promise<StoredCredentials | null>;
9
+ export declare function saveCredentials(data: StoredCredentials): Promise<void>;
10
+ export declare function clearCredentials(): Promise<void>;
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runCli(): Promise<void>;
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ async function startMcpServer() {
35
35
  await server.connect(transport);
36
36
  }
37
37
  function printMainHelp() {
38
- console.log(`kaneo-mcp — Kaneo MCP server (stdio transport)
38
+ console.log(`kaneo-mcp: Kaneo MCP server (stdio transport)
39
39
 
40
40
  Usage:
41
41
  npx @kaneo/mcp Interactive installer (terminal only; no global install)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
File without changes
@@ -0,0 +1,11 @@
1
+ export type ParsedInstallArgs = {
2
+ target?: string;
3
+ output?: string;
4
+ name: string;
5
+ yes: boolean;
6
+ apiUrl?: string;
7
+ projectDir: string;
8
+ help: boolean;
9
+ };
10
+ export declare function parseInstallArgs(argv: string[]): ParsedInstallArgs;
11
+ export declare function runInstall(argv: string[]): Promise<void>;
@@ -273,7 +273,7 @@ export async function runInstall(argv) {
273
273
  console.log("\nRestart your MCP client (or reload the window) if needed.");
274
274
  }
275
275
  function printInstallHelp() {
276
- console.log(`kaneo-mcp install — register Kaneo in an MCP client config
276
+ console.log(`kaneo-mcp install: register Kaneo in an MCP client config
277
277
 
278
278
  Usage:
279
279
  kaneo-mcp install [options]
@@ -0,0 +1,10 @@
1
+ export type McpServerEntry = {
2
+ command: string;
3
+ args: string[];
4
+ env?: Record<string, string>;
5
+ };
6
+ /**
7
+ * Merges or replaces `mcpServers[serverName]` and returns formatted JSON.
8
+ * Preserves other top-level keys and other MCP server entries.
9
+ */
10
+ export declare function mergeMcpServerEntry(existingJson: string | null, serverName: string, serverConfig: McpServerEntry): string;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Absolute path to this package's `dist/index.js` (the MCP stdio entry).
3
+ * Resolved from `dist/install/resolve-entry.js` at runtime.
4
+ */
5
+ export declare function resolvePackageEntryPath(): string;
@@ -0,0 +1,39 @@
1
+ export type InstallTargetId = "cursor-user" | "cursor-project" | "claude-desktop" | "custom";
2
+ export type InstallTarget = {
3
+ id: InstallTargetId;
4
+ label: string;
5
+ description: string;
6
+ };
7
+ /**
8
+ * Validates a non-interactive custom MCP config path (same rules as the install wizard).
9
+ * Returns the trimmed absolute path on success.
10
+ */
11
+ export declare function validateCustomConfigPathInput(raw: string): {
12
+ ok: true;
13
+ path: string;
14
+ } | {
15
+ ok: false;
16
+ message: string;
17
+ };
18
+ export declare const INSTALL_TARGETS: readonly [{
19
+ readonly id: "cursor-user";
20
+ readonly label: "Cursor (user-wide)";
21
+ readonly description: "~/.cursor/mcp.json, available in all projects";
22
+ }, {
23
+ readonly id: "cursor-project";
24
+ readonly label: "Cursor (this project only)";
25
+ readonly description: ".cursor/mcp.json in the current directory";
26
+ }, {
27
+ readonly id: "claude-desktop";
28
+ readonly label: "Claude Desktop";
29
+ readonly description: "claude_desktop_config.json for the Claude app";
30
+ }, {
31
+ readonly id: "custom";
32
+ readonly label: "Custom file path";
33
+ readonly description: "Any JSON file you choose (advanced)";
34
+ }];
35
+ export declare function getClaudeDesktopConfigPath(): string;
36
+ export declare function resolveTargetConfigPath(id: InstallTargetId, options: {
37
+ cwd: string;
38
+ customPath?: string;
39
+ }): string;
@@ -21,7 +21,7 @@ export const INSTALL_TARGETS = [
21
21
  {
22
22
  id: "cursor-user",
23
23
  label: "Cursor (user-wide)",
24
- description: "~/.cursor/mcp.json — available in all projects",
24
+ description: "~/.cursor/mcp.json, available in all projects",
25
25
  },
26
26
  {
27
27
  id: "cursor-project",
@@ -0,0 +1,4 @@
1
+ import { type InstallTargetId } from "./targets.js";
2
+ export declare function promptTargetSelect(): Promise<InstallTargetId[]>;
3
+ export declare function promptCustomConfigPath(): Promise<string>;
4
+ export declare function promptConfirmOverwrite(serverName: string, configPath: string): Promise<boolean>;
@@ -0,0 +1,14 @@
1
+ import type { AuthService } from "../auth/auth-service.js";
2
+ export type Json = null | boolean | number | string | Json[] | {
3
+ [key: string]: Json;
4
+ };
5
+ export declare class KaneoClient {
6
+ readonly baseUrl: string;
7
+ private readonly auth;
8
+ constructor(options: {
9
+ baseUrl: string;
10
+ auth: AuthService;
11
+ });
12
+ private authorizedFetch;
13
+ json<T = Json>(path: string, init?: RequestInit): Promise<T>;
14
+ }
@@ -18,7 +18,9 @@ export class KaneoClient {
18
18
  ? AbortSignal.any([init.signal, timeoutSignal])
19
19
  : timeoutSignal;
20
20
  const res = await fetch(url, { ...init, headers, signal });
21
- if (res.status === 401 && !didRetry) {
21
+ // With a static API key there is nothing to refresh, so surface the 401
22
+ // instead of looping back into the interactive device flow.
23
+ if (res.status === 401 && !didRetry && !this.auth.usingApiKey) {
22
24
  await this.auth.clearToken();
23
25
  return this.authorizedFetch(path, init, true);
24
26
  }
@@ -0,0 +1,19 @@
1
+ declare const PRIORITIES: readonly ["no-priority", "low", "medium", "high", "urgent"];
2
+ export type TaskPriority = (typeof PRIORITIES)[number];
3
+ export declare function isTaskPriority(v: string): v is TaskPriority;
4
+ export type TaskUpdatePatch = {
5
+ title?: string;
6
+ description?: string | null;
7
+ status?: string;
8
+ priority?: TaskPriority;
9
+ projectId?: string;
10
+ position?: number;
11
+ startDate?: string | null;
12
+ dueDate?: string | null;
13
+ userId?: string | null;
14
+ };
15
+ /**
16
+ * Builds the JSON body for `PUT /api/task/:id` from an existing task plus a patch.
17
+ */
18
+ export declare function buildFullTaskUpdateBody(existing: Record<string, unknown>, patch: TaskUpdatePatch): Record<string, string | number | undefined>;
19
+ export {};
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createMcpServer(): McpServer;
package/dist/server.js CHANGED
@@ -9,7 +9,8 @@ const { version: packageVersion } = require("../package.json");
9
9
  export function createMcpServer() {
10
10
  const baseUrl = normalizeBaseUrl(process.env.KANEO_API_URL || "http://localhost:1337");
11
11
  const clientId = process.env.KANEO_MCP_CLIENT_ID || "kaneo-mcp";
12
- const auth = new AuthService({ baseUrl, clientId });
12
+ const apiKey = process.env.KANEO_API_KEY || undefined;
13
+ const auth = new AuthService({ baseUrl, clientId, apiKey });
13
14
  const client = new KaneoClient({ baseUrl, auth });
14
15
  const server = new McpServer({
15
16
  name: "kaneo-mcp",
@@ -0,0 +1,5 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { KaneoClient } from "../kaneo/client.js";
3
+ export declare function registerTools(server: McpServer, ctx: {
4
+ client: KaneoClient;
5
+ }): void;
@@ -255,6 +255,22 @@ export function registerTools(server, ctx) {
255
255
  method: "POST",
256
256
  body: JSON.stringify({ content: args.content }),
257
257
  })));
258
+ server.registerTool("update_task_comment", {
259
+ description: "Update one of your comments on a task.",
260
+ inputSchema: z.object({
261
+ commentId: nonEmptyString,
262
+ content: nonEmptyString,
263
+ }),
264
+ }, async (args) => run(() => client.json(`/api/comment/${encodeURIComponent(args.commentId)}`, {
265
+ method: "PUT",
266
+ body: JSON.stringify({ content: args.content }),
267
+ })));
268
+ server.registerTool("delete_task_comment", {
269
+ description: "Delete one of your comments from a task.",
270
+ inputSchema: z.object({ commentId: nonEmptyString }),
271
+ }, async (args) => run(() => client.json(`/api/comment/${encodeURIComponent(args.commentId)}`, {
272
+ method: "DELETE",
273
+ })));
258
274
  server.registerTool("list_workspace_labels", {
259
275
  description: "List labels defined in a workspace.",
260
276
  inputSchema: z.object({ workspaceId: nonEmptyString }),
@@ -292,4 +308,43 @@ export function registerTools(server, ctx) {
292
308
  }, async (args) => run(() => client.json(`/api/label/${encodeURIComponent(args.labelId)}/task`, {
293
309
  method: "DELETE",
294
310
  })));
311
+ server.registerTool("create_task_relation", {
312
+ description: "Create a relation between two tasks. relationType: 'subtask' (sourceTaskId is the parent, targetTaskId the child), 'blocks' (sourceTaskId blocks targetTaskId), or 'related' (bidirectional).",
313
+ inputSchema: z.object({
314
+ sourceTaskId: nonEmptyString,
315
+ targetTaskId: nonEmptyString,
316
+ relationType: z.enum(["subtask", "blocks", "related"]),
317
+ }),
318
+ }, async (args) => run(() => client.json("/api/task-relation", {
319
+ method: "POST",
320
+ body: JSON.stringify({
321
+ sourceTaskId: args.sourceTaskId,
322
+ targetTaskId: args.targetTaskId,
323
+ relationType: args.relationType,
324
+ }),
325
+ })));
326
+ server.registerTool("get_task_relations", {
327
+ description: "List all relations (subtask/blocks/related) involving a task.",
328
+ inputSchema: z.object({ taskId: nonEmptyString }),
329
+ }, async (args) => run(() => client.json(`/api/task-relation/${encodeURIComponent(args.taskId)}`, {
330
+ method: "GET",
331
+ })));
332
+ server.registerTool("delete_task_relation", {
333
+ description: "Delete a task relation by its relation ID.",
334
+ inputSchema: z.object({ id: nonEmptyString }),
335
+ }, async (args) => run(() => client.json(`/api/task-relation/${encodeURIComponent(args.id)}`, {
336
+ method: "DELETE",
337
+ })));
338
+ server.registerTool("delete_label", {
339
+ description: "Delete a label by ID. Only task-associated labels can be deleted; workspace-level labels (taskId null) are rejected by the API.",
340
+ inputSchema: z.object({ id: nonEmptyString }),
341
+ }, async (args) => run(async () => {
342
+ const label = (await client.json(`/api/label/${encodeURIComponent(args.id)}`, { method: "GET" }));
343
+ if (!label?.taskId) {
344
+ throw new Error("Label is not associated with a task and cannot be deleted (workspace-level labels are not deletable via this endpoint).");
345
+ }
346
+ return client.json(`/api/label/${encodeURIComponent(args.id)}`, {
347
+ method: "DELETE",
348
+ });
349
+ }));
295
350
  }
@@ -0,0 +1,3 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare function textResult(data: unknown, isError?: boolean): CallToolResult;
3
+ export declare function errorResult(message: string): CallToolResult;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Kaneo API base URL without trailing slash and without `/api` suffix.
3
+ */
4
+ export declare function normalizeBaseUrl(raw: string): string;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@kaneo/mcp",
3
- "version": "0.1.5",
4
- "description": "Model Context Protocol (stdio) server for Kaneo — tasks, projects, labels, and device authorization",
3
+ "version": "0.1.7",
4
+ "description": "Official MCP (Model Context Protocol) server for Kaneo, the open source project management platform: manage tasks, projects, and labels from Claude, Cursor, and other MCP clients",
5
5
  "license": "MIT",
6
- "homepage": "https://github.com/usekaneo/kaneo/",
6
+ "author": "Kaneo (https://kaneo.app)",
7
+ "homepage": "https://docs.kaneo.app/core/integrations/mcp",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/usekaneo/kaneo/",
@@ -13,6 +14,12 @@
13
14
  "bin": {
14
15
  "kaneo-mcp": "dist/index.js"
15
16
  },
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/server.js",
20
+ "types": "./dist/server.d.ts"
21
+ }
22
+ },
16
23
  "publishConfig": {
17
24
  "access": "public"
18
25
  },
@@ -23,8 +30,15 @@
23
30
  ],
24
31
  "keywords": [
25
32
  "mcp",
33
+ "mcp-server",
26
34
  "model-context-protocol",
27
35
  "kaneo",
36
+ "kaneo-mcp",
37
+ "project-management",
38
+ "tasks",
39
+ "kanban",
40
+ "ai-agents",
41
+ "claude",
28
42
  "stdio"
29
43
  ],
30
44
  "scripts": {
@@ -37,17 +51,17 @@
37
51
  "test:watch": "vitest --config vitest.config.ts"
38
52
  },
39
53
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "^1.26.0",
54
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
55
  "open": "^11.0.0",
42
56
  "prompts": "^2.4.2",
43
- "zod": "^4.3.6"
57
+ "zod": "^4.4.3"
44
58
  },
45
59
  "devDependencies": {
46
60
  "@kaneo/typescript-config": "workspace:*",
47
61
  "@types/node": "^25.3.5",
48
62
  "@types/prompts": "^2.4.9",
49
- "tsx": "^4.21.0",
63
+ "tsx": "^4.23.1",
50
64
  "typescript": "^5.9.3",
51
- "vitest": "^4.1.2"
65
+ "vitest": "^4.1.10"
52
66
  }
53
67
  }