@kanbodev/mcp 1.1.5 → 1.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
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@kanbodev/mcp.svg)](https://www.npmjs.com/package/@kanbodev/mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- AI-native project management via [Model Context Protocol](https://modelcontextprotocol.io). Connect your AI assistant (Claude, Cursor, VS Code, Claude Code) to Kanbo and manage tickets, projects, releases, and workflows through natural conversation.
6
+ AI-native project management via [Model Context Protocol](https://modelcontextprotocol.io). Connect your AI assistant (VS Code, Cursor, Windsurf, Claude Code, Claude Desktop) to Kanbo and manage tickets, projects, releases, and workflows through natural conversation.
7
7
 
8
8
  ## Quick Setup
9
9
 
@@ -15,7 +15,7 @@ That's it. This single command will:
15
15
 
16
16
  1. Open your browser to sign in and select your organization
17
17
  2. Store credentials locally at `~/.kanbo/config.json`
18
- 3. **Auto-register the MCP server** with detected editors (VS Code, Cursor, Claude Desktop, Claude Code CLI)
18
+ 3. **Auto-register the MCP server** with detected editors (VS Code, VS Code Insiders, Cursor, Windsurf, Claude Code, Claude Desktop, Claude Code CLI)
19
19
 
20
20
  Reload your editor and start using Kanbo tools immediately.
21
21
 
package/dist/index.js CHANGED
@@ -29,6 +29,247 @@ var __export = (target, all) => {
29
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
30
30
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
31
31
 
32
+ // src/lib/errors.ts
33
+ function isAppError(error) {
34
+ return error instanceof AppError;
35
+ }
36
+ function isKanboAPIError(error) {
37
+ return error instanceof KanboAPIError;
38
+ }
39
+ function getErrorCode(error) {
40
+ if (isAppError(error)) {
41
+ return error.code;
42
+ }
43
+ return ERROR_CODES.INTERNAL_ERROR;
44
+ }
45
+ var ERROR_CODES, AppError, ValidationError, KanboAPIError, APITimeoutError, APINotConfiguredError, InvalidAPIKeyError, APIKeyRevokedError, APIKeyExpiredError;
46
+ var init_errors = __esm(() => {
47
+ ERROR_CODES = {
48
+ INTERNAL_ERROR: "INTERNAL_ERROR",
49
+ VALIDATION_ERROR: "VALIDATION_ERROR",
50
+ NOT_FOUND: "NOT_FOUND",
51
+ UNAUTHORIZED: "UNAUTHORIZED",
52
+ FORBIDDEN: "FORBIDDEN",
53
+ BAD_REQUEST: "BAD_REQUEST",
54
+ CONFLICT: "CONFLICT",
55
+ TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
56
+ TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
57
+ INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
58
+ API_ERROR: "API_ERROR",
59
+ API_TIMEOUT: "API_TIMEOUT",
60
+ API_RATE_LIMITED: "API_RATE_LIMITED",
61
+ API_UNAVAILABLE: "API_UNAVAILABLE",
62
+ API_NOT_CONFIGURED: "API_NOT_CONFIGURED",
63
+ INVALID_API_KEY: "INVALID_API_KEY",
64
+ API_KEY_REQUIRED: "API_KEY_REQUIRED",
65
+ API_KEY_REVOKED: "API_KEY_REVOKED",
66
+ API_KEY_EXPIRED: "API_KEY_EXPIRED",
67
+ NO_ORG_SELECTED: "NO_ORG_SELECTED",
68
+ PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
69
+ NOT_PROJECT_MEMBER: "NOT_PROJECT_MEMBER",
70
+ TICKET_NOT_FOUND: "TICKET_NOT_FOUND",
71
+ INVALID_STATUS_TRANSITION: "INVALID_STATUS_TRANSITION",
72
+ STATUS_NOT_FOUND: "STATUS_NOT_FOUND",
73
+ COMMENT_NOT_FOUND: "COMMENT_NOT_FOUND",
74
+ NOT_COMMENT_CREATOR: "NOT_COMMENT_CREATOR",
75
+ TAG_NOT_FOUND: "TAG_NOT_FOUND",
76
+ TAG_ALREADY_EXISTS: "TAG_ALREADY_EXISTS",
77
+ BATCH_SIZE_EXCEEDED: "BATCH_SIZE_EXCEEDED",
78
+ BATCH_PARTIAL_FAILURE: "BATCH_PARTIAL_FAILURE"
79
+ };
80
+ AppError = class AppError extends Error {
81
+ code;
82
+ statusCode;
83
+ details;
84
+ isRetryable;
85
+ constructor(code, message, statusCode = 500, details, isRetryable = false) {
86
+ super(message);
87
+ this.name = "AppError";
88
+ this.code = code;
89
+ this.statusCode = statusCode;
90
+ this.details = details;
91
+ this.isRetryable = isRetryable;
92
+ if (Error.captureStackTrace) {
93
+ Error.captureStackTrace(this, this.constructor);
94
+ }
95
+ }
96
+ toJSON() {
97
+ return {
98
+ name: this.name,
99
+ code: this.code,
100
+ message: this.message,
101
+ statusCode: this.statusCode,
102
+ details: this.details,
103
+ isRetryable: this.isRetryable
104
+ };
105
+ }
106
+ };
107
+ ValidationError = class ValidationError extends AppError {
108
+ constructor(message, details) {
109
+ super(ERROR_CODES.VALIDATION_ERROR, message, 400, details);
110
+ }
111
+ };
112
+ KanboAPIError = class KanboAPIError extends AppError {
113
+ originalError;
114
+ constructor(message, statusCode = 500, originalError, details) {
115
+ const code = KanboAPIError.statusToErrorCode(statusCode);
116
+ const isRetryable = KanboAPIError.isStatusRetryable(statusCode);
117
+ super(code, message, statusCode, details, isRetryable);
118
+ this.name = "KanboAPIError";
119
+ this.originalError = originalError;
120
+ }
121
+ static statusToErrorCode(status) {
122
+ switch (status) {
123
+ case 400:
124
+ return ERROR_CODES.BAD_REQUEST;
125
+ case 401:
126
+ return ERROR_CODES.UNAUTHORIZED;
127
+ case 403:
128
+ return ERROR_CODES.FORBIDDEN;
129
+ case 404:
130
+ return ERROR_CODES.NOT_FOUND;
131
+ case 409:
132
+ return ERROR_CODES.CONFLICT;
133
+ case 422:
134
+ return ERROR_CODES.VALIDATION_ERROR;
135
+ case 429:
136
+ return ERROR_CODES.API_RATE_LIMITED;
137
+ case 503:
138
+ return ERROR_CODES.API_UNAVAILABLE;
139
+ default:
140
+ return ERROR_CODES.API_ERROR;
141
+ }
142
+ }
143
+ static isStatusRetryable(status) {
144
+ return status >= 500 || status === 429;
145
+ }
146
+ };
147
+ APITimeoutError = class APITimeoutError extends AppError {
148
+ constructor(timeoutMs) {
149
+ super(ERROR_CODES.API_TIMEOUT, `API request timed out after ${timeoutMs}ms`, 408, { timeoutMs }, true);
150
+ }
151
+ };
152
+ APINotConfiguredError = class APINotConfiguredError extends AppError {
153
+ constructor(message = "API client not configured") {
154
+ super(ERROR_CODES.API_NOT_CONFIGURED, message, 500);
155
+ }
156
+ };
157
+ InvalidAPIKeyError = class InvalidAPIKeyError extends AppError {
158
+ constructor(message = 'Invalid API key. Keys should start with "kanbo_pat_".') {
159
+ super(ERROR_CODES.INVALID_API_KEY, message, 401);
160
+ }
161
+ };
162
+ APIKeyRevokedError = class APIKeyRevokedError extends AppError {
163
+ constructor() {
164
+ super(ERROR_CODES.API_KEY_REVOKED, "This API key has been revoked. Please generate a new key from the Kanbo web app.", 401);
165
+ }
166
+ };
167
+ APIKeyExpiredError = class APIKeyExpiredError extends AppError {
168
+ constructor() {
169
+ super(ERROR_CODES.API_KEY_EXPIRED, "This API key has expired. Please generate a new key from the Kanbo web app.", 401);
170
+ }
171
+ };
172
+ });
173
+
174
+ // src/config/constants.ts
175
+ import * as fs from "node:fs";
176
+ import * as path from "node:path";
177
+ import * as os from "node:os";
178
+ function loadConfigFile() {
179
+ try {
180
+ const configPath = path.join(os.homedir(), ".kanbo", "config.json");
181
+ if (!fs.existsSync(configPath)) {
182
+ return null;
183
+ }
184
+ const content = fs.readFileSync(configPath, "utf-8");
185
+ const config = JSON.parse(content);
186
+ if (config.version !== 1 || !config.apiKey) {
187
+ return null;
188
+ }
189
+ return config;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ function getApiKey() {
195
+ if (process.env.KANBO_API_KEY) {
196
+ return process.env.KANBO_API_KEY;
197
+ }
198
+ const config = loadConfigFile();
199
+ return config?.apiKey;
200
+ }
201
+ function getApiUrl() {
202
+ if (process.env.KANBO_API_URL) {
203
+ return process.env.KANBO_API_URL;
204
+ }
205
+ const config = loadConfigFile();
206
+ return config?.apiUrl || "https://api.kanbo.dev";
207
+ }
208
+ function getOrgId() {
209
+ if (process.env.KANBO_ORG_ID) {
210
+ return process.env.KANBO_ORG_ID;
211
+ }
212
+ const config = loadConfigFile();
213
+ return config?.orgId;
214
+ }
215
+ var CONFIG, SERVER_INFO, PAT_CONFIG, LIMITS, TICKET_LOCATIONS, TICKET_PRIORITIES, TICKET_SIZES, TAG_COLORS;
216
+ var init_constants = __esm(() => {
217
+ init_errors();
218
+ CONFIG = {
219
+ KANBO_API_URL: getApiUrl(),
220
+ KANBO_API_KEY: getApiKey(),
221
+ TRANSPORT: process.env.TRANSPORT || "stdio",
222
+ PORT: parseInt(process.env.PORT || "8081", 10),
223
+ MAX_BODY_SIZE: 1048576,
224
+ CORS_ORIGINS: process.env.KANBO_CORS_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean) || [],
225
+ KANBO_ORG_ID: getOrgId(),
226
+ USE_OIDC_AUTH: process.env.USE_OIDC_AUTH === "true"
227
+ };
228
+ SERVER_INFO = {
229
+ name: "kanbo-mcp",
230
+ version: "1.1.7",
231
+ description: "MCP server for Kanbo project management"
232
+ };
233
+ PAT_CONFIG = {
234
+ PREFIX: "kanbo_pat_",
235
+ TOKEN_LENGTH: 58
236
+ };
237
+ LIMITS = {
238
+ TITLE_MIN: 1,
239
+ TITLE_MAX: 200,
240
+ DESCRIPTION_MAX: 5000,
241
+ ACCEPTANCE_CRITERIA_MAX: 1e4,
242
+ COMMENT_MAX: 2000,
243
+ NAME_MIN: 1,
244
+ NAME_MAX: 100,
245
+ TAG_NAME_MAX: 30,
246
+ PAGE_SIZE_DEFAULT: 20,
247
+ PAGE_SIZE_MAX: 100,
248
+ BATCH_SIZE_MAX: 50
249
+ };
250
+ TICKET_LOCATIONS = ["board", "backlog", "completed", "abandoned"];
251
+ TICKET_PRIORITIES = ["critical", "high", "medium", "low"];
252
+ TICKET_SIZES = ["xs", "s", "m", "l", "xl"];
253
+ TAG_COLORS = [
254
+ "gray",
255
+ "sky",
256
+ "blue",
257
+ "indigo",
258
+ "emerald",
259
+ "green",
260
+ "teal",
261
+ "cyan",
262
+ "amber",
263
+ "yellow",
264
+ "orange",
265
+ "red",
266
+ "violet",
267
+ "purple",
268
+ "fuchsia",
269
+ "pink"
270
+ ];
271
+ });
272
+
32
273
  // node_modules/ajv/dist/compile/codegen/code.js
33
274
  var require_code = __commonJS((exports) => {
34
275
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -15534,6 +15775,7 @@ function startCallbackServer(port, expectedState, timeoutMs = 120000) {
15534
15775
  res.end(successHtml(email2, orgName));
15535
15776
  if (!resolved) {
15536
15777
  resolved = true;
15778
+ server.closeAllConnections();
15537
15779
  server.close();
15538
15780
  resolve({
15539
15781
  success: true,
@@ -15555,6 +15797,7 @@ function startCallbackServer(port, expectedState, timeoutMs = 120000) {
15555
15797
  setTimeout(() => {
15556
15798
  if (!resolved) {
15557
15799
  resolved = true;
15800
+ server.closeAllConnections();
15558
15801
  server.close();
15559
15802
  resolve({
15560
15803
  success: false,
@@ -15617,10 +15860,14 @@ function getConfigPath() {
15617
15860
  return CONFIG_PATH;
15618
15861
  }
15619
15862
  function maskApiKey(apiKey) {
15620
- if (!apiKey || apiKey.length < 15) {
15863
+ if (!apiKey || apiKey.length < 10) {
15621
15864
  return "***";
15622
15865
  }
15623
- return apiKey.substring(0, 14) + "...";
15866
+ const prefixEnd = apiKey.indexOf("_", apiKey.indexOf("_") + 1);
15867
+ if (prefixEnd !== -1) {
15868
+ return apiKey.substring(0, prefixEnd + 1) + "****";
15869
+ }
15870
+ return apiKey.substring(0, 4) + "****";
15624
15871
  }
15625
15872
  var CONFIG_DIR, CONFIG_PATH;
15626
15873
  var init_config = __esm(() => {
@@ -15639,20 +15886,39 @@ function getVscodeMcpPath() {
15639
15886
  return path3.join(os3.homedir(), "Library", "Application Support", "Code", "User", "mcp.json");
15640
15887
  }
15641
15888
  if (platform === "win32") {
15642
- return path3.join(process.env.APPDATA || "", "Code", "User", "mcp.json");
15889
+ const appData = process.env.APPDATA || path3.join(os3.homedir(), "AppData", "Roaming");
15890
+ return path3.join(appData, "Code", "User", "mcp.json");
15643
15891
  }
15644
15892
  return path3.join(os3.homedir(), ".config", "Code", "User", "mcp.json");
15645
15893
  }
15894
+ function getVscodeInsidersMcpPath() {
15895
+ const platform = process.platform;
15896
+ if (platform === "darwin") {
15897
+ return path3.join(os3.homedir(), "Library", "Application Support", "Code - Insiders", "User", "mcp.json");
15898
+ }
15899
+ if (platform === "win32") {
15900
+ const appData = process.env.APPDATA || path3.join(os3.homedir(), "AppData", "Roaming");
15901
+ return path3.join(appData, "Code - Insiders", "User", "mcp.json");
15902
+ }
15903
+ return path3.join(os3.homedir(), ".config", "Code - Insiders", "User", "mcp.json");
15904
+ }
15646
15905
  function getCursorMcpPath() {
15647
15906
  return path3.join(os3.homedir(), ".cursor", "mcp.json");
15648
15907
  }
15908
+ function getWindsurfMcpPath() {
15909
+ return path3.join(os3.homedir(), ".codeium", "windsurf", "mcp_config.json");
15910
+ }
15911
+ function getClaudeCodeSettingsPath() {
15912
+ return path3.join(os3.homedir(), ".claude", "settings.json");
15913
+ }
15649
15914
  function getClaudeDesktopMcpPath() {
15650
15915
  const platform = process.platform;
15651
15916
  if (platform === "darwin") {
15652
15917
  return path3.join(os3.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
15653
15918
  }
15654
15919
  if (platform === "win32") {
15655
- return path3.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
15920
+ const appData = process.env.APPDATA || path3.join(os3.homedir(), "AppData", "Roaming");
15921
+ return path3.join(appData, "Claude", "claude_desktop_config.json");
15656
15922
  }
15657
15923
  return path3.join(os3.homedir(), ".config", "Claude", "claude_desktop_config.json");
15658
15924
  }
@@ -15664,8 +15930,14 @@ function registerInConfigFile(configPath, hostName, serversKey = "servers") {
15664
15930
  }
15665
15931
  let config3 = {};
15666
15932
  if (fs3.existsSync(configPath)) {
15667
- const content = fs3.readFileSync(configPath, "utf-8");
15668
- config3 = JSON.parse(content);
15933
+ try {
15934
+ const content = fs3.readFileSync(configPath, "utf-8");
15935
+ config3 = JSON.parse(content);
15936
+ } catch {
15937
+ const backupPath = configPath + ".backup";
15938
+ fs3.copyFileSync(configPath, backupPath);
15939
+ config3 = {};
15940
+ }
15669
15941
  }
15670
15942
  const servers = config3[serversKey] || {};
15671
15943
  if (servers[SERVER_NAME]) {
@@ -15692,7 +15964,7 @@ function registerInConfigFile(configPath, hostName, serversKey = "servers") {
15692
15964
  }
15693
15965
  function isClaudeCliAvailable() {
15694
15966
  try {
15695
- execFileSync("claude", ["--version"], { stdio: "pipe" });
15967
+ execFileSync("claude", ["--version"], { stdio: "pipe", ...shellOpt });
15696
15968
  return true;
15697
15969
  } catch {
15698
15970
  return false;
@@ -15705,7 +15977,8 @@ function registerWithClaudeCli() {
15705
15977
  return { host: hostName, success: false, message: "Claude Code CLI not detected" };
15706
15978
  }
15707
15979
  execFileSync("claude", ["mcp", "add", SERVER_NAME, "--", "npx", "@kanbodev/mcp"], {
15708
- stdio: "pipe"
15980
+ stdio: "pipe",
15981
+ ...shellOpt
15709
15982
  });
15710
15983
  return { host: hostName, success: true, message: "Registered via Claude Code CLI" };
15711
15984
  } catch (error2) {
@@ -15719,7 +15992,10 @@ function registerWithClaudeCli() {
15719
15992
  function registerMcpServer() {
15720
15993
  const results = [];
15721
15994
  results.push(registerInConfigFile(getVscodeMcpPath(), "VS Code"));
15995
+ results.push(registerInConfigFile(getVscodeInsidersMcpPath(), "VS Code Insiders"));
15722
15996
  results.push(registerInConfigFile(getCursorMcpPath(), "Cursor"));
15997
+ results.push(registerInConfigFile(getWindsurfMcpPath(), "Windsurf", "mcpServers"));
15998
+ results.push(registerInConfigFile(getClaudeCodeSettingsPath(), "Claude Code", "mcpServers"));
15723
15999
  results.push(registerInConfigFile(getClaudeDesktopMcpPath(), "Claude Desktop", "mcpServers"));
15724
16000
  results.push(registerWithClaudeCli());
15725
16001
  return results;
@@ -15749,8 +16025,9 @@ function printRegistrationResults(results) {
15749
16025
  return anySuccess;
15750
16026
  }
15751
16027
  function printManualInstructions() {
15752
- console.log(" To register the MCP server manually, add this to your mcp.json:");
16028
+ console.log(" To register the MCP server manually, add this to your editor config:");
15753
16029
  console.log("");
16030
+ console.log(" For VS Code / Cursor (mcp.json):");
15754
16031
  console.log(" {");
15755
16032
  console.log(' "servers": {');
15756
16033
  console.log(' "kanbodev": {');
@@ -15761,21 +16038,36 @@ function printManualInstructions() {
15761
16038
  console.log(" }");
15762
16039
  console.log(" }");
15763
16040
  console.log("");
16041
+ console.log(" For Claude Desktop / Windsurf / Claude Code:");
16042
+ console.log(" {");
16043
+ console.log(' "mcpServers": {');
16044
+ console.log(' "kanbodev": {');
16045
+ console.log(' "type": "stdio",');
16046
+ console.log(' "command": "npx",');
16047
+ console.log(' "args": ["@kanbodev/mcp"]');
16048
+ console.log(" }");
16049
+ console.log(" }");
16050
+ console.log(" }");
16051
+ console.log("");
15764
16052
  console.log(" Config file locations:");
15765
- console.log(` VS Code: ${getVscodeMcpPath()}`);
15766
- console.log(` Cursor: ${getCursorMcpPath()}`);
15767
- console.log(` Claude Desktop: ${getClaudeDesktopMcpPath()}`);
16053
+ console.log(` VS Code: ${getVscodeMcpPath()}`);
16054
+ console.log(` VS Code Insiders: ${getVscodeInsidersMcpPath()}`);
16055
+ console.log(` Cursor: ${getCursorMcpPath()}`);
16056
+ console.log(` Windsurf: ${getWindsurfMcpPath()}`);
16057
+ console.log(` Claude Code: ${getClaudeCodeSettingsPath()}`);
16058
+ console.log(` Claude Desktop: ${getClaudeDesktopMcpPath()}`);
15768
16059
  console.log("");
15769
16060
  console.log(" Or register via Claude Code CLI:");
15770
16061
  console.log(" claude mcp add kanbodev -- npx @kanbodev/mcp");
15771
16062
  }
15772
- var SERVER_NAME = "kanbodev", MCP_SERVER_ENTRY;
16063
+ var SERVER_NAME = "kanbodev", MCP_SERVER_ENTRY, shellOpt;
15773
16064
  var init_register = __esm(() => {
15774
16065
  MCP_SERVER_ENTRY = {
15775
16066
  type: "stdio",
15776
16067
  command: "npx",
15777
16068
  args: ["@kanbodev/mcp"]
15778
16069
  };
16070
+ shellOpt = process.platform === "win32" ? { shell: true } : {};
15779
16071
  });
15780
16072
 
15781
16073
  // node_modules/is-docker/index.js
@@ -16434,7 +16726,6 @@ __export(exports_login, {
16434
16726
  login: () => login
16435
16727
  });
16436
16728
  import * as crypto2 from "node:crypto";
16437
- import { createRequire as createRequire2 } from "node:module";
16438
16729
  async function login() {
16439
16730
  console.log(`
16440
16731
  Kanbo CLI Login
@@ -16520,16 +16811,16 @@ async function login() {
16520
16811
  printRegistrationResults(registrationResults);
16521
16812
  console.log("");
16522
16813
  }
16523
- var KANBO_WEB_URL, DEFAULT_API_URL, PORT_RANGE, require2, CLI_VERSION;
16814
+ var KANBO_WEB_URL, DEFAULT_API_URL, PORT_RANGE, CLI_VERSION;
16524
16815
  var init_login = __esm(() => {
16525
16816
  init_callback_server();
16526
16817
  init_config();
16527
16818
  init_register();
16819
+ init_constants();
16528
16820
  KANBO_WEB_URL = process.env.KANBO_WEB_URL || "https://kanbo.dev";
16529
16821
  DEFAULT_API_URL = process.env.KANBO_API_URL || "https://api.kanbo.dev";
16530
16822
  PORT_RANGE = { start: 9876, end: 9899 };
16531
- require2 = createRequire2(import.meta.url);
16532
- CLI_VERSION = require2("../../package.json").version;
16823
+ CLI_VERSION = SERVER_INFO.version;
16533
16824
  });
16534
16825
 
16535
16826
  // src/cli/logout.ts
@@ -16622,20 +16913,19 @@ async function install() {
16622
16913
  Kanbo MCP — Install
16623
16914
  `);
16624
16915
  const config3 = loadConfig();
16625
- if (!config3) {
16626
- console.log(" You are not logged in. Please authenticate first:");
16627
- console.log(" npx @kanbodev/mcp login");
16628
- console.log("");
16629
- console.log(" Or register the MCP server manually:");
16630
- console.log("");
16631
- printManualInstructions();
16632
- console.log("");
16633
- return;
16916
+ if (config3) {
16917
+ console.log(` Logged in as ${config3.userEmail} (${config3.orgName})`);
16918
+ } else {
16919
+ console.log(" Not logged in yet — registering MCP server entry.");
16920
+ console.log(' Run "npx @kanbodev/mcp login" to authenticate after install.');
16634
16921
  }
16635
- console.log(` Logged in as ${config3.userEmail} (${config3.orgName})`);
16636
16922
  console.log("");
16637
16923
  const results = registerMcpServer();
16638
- printRegistrationResults(results);
16924
+ const anySuccess = printRegistrationResults(results);
16925
+ if (!anySuccess) {
16926
+ console.log("");
16927
+ printManualInstructions();
16928
+ }
16639
16929
  console.log("");
16640
16930
  }
16641
16931
  var init_install = __esm(() => {
@@ -16643,243 +16933,8 @@ var init_install = __esm(() => {
16643
16933
  init_register();
16644
16934
  });
16645
16935
 
16646
- // src/config/constants.ts
16647
- import * as fs from "node:fs";
16648
- import * as path from "node:path";
16649
- import * as os from "node:os";
16650
-
16651
- // src/lib/errors.ts
16652
- var ERROR_CODES = {
16653
- INTERNAL_ERROR: "INTERNAL_ERROR",
16654
- VALIDATION_ERROR: "VALIDATION_ERROR",
16655
- NOT_FOUND: "NOT_FOUND",
16656
- UNAUTHORIZED: "UNAUTHORIZED",
16657
- FORBIDDEN: "FORBIDDEN",
16658
- BAD_REQUEST: "BAD_REQUEST",
16659
- CONFLICT: "CONFLICT",
16660
- TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
16661
- TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
16662
- INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
16663
- API_ERROR: "API_ERROR",
16664
- API_TIMEOUT: "API_TIMEOUT",
16665
- API_RATE_LIMITED: "API_RATE_LIMITED",
16666
- API_UNAVAILABLE: "API_UNAVAILABLE",
16667
- API_NOT_CONFIGURED: "API_NOT_CONFIGURED",
16668
- INVALID_API_KEY: "INVALID_API_KEY",
16669
- API_KEY_REQUIRED: "API_KEY_REQUIRED",
16670
- API_KEY_REVOKED: "API_KEY_REVOKED",
16671
- API_KEY_EXPIRED: "API_KEY_EXPIRED",
16672
- NO_ORG_SELECTED: "NO_ORG_SELECTED",
16673
- PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
16674
- NOT_PROJECT_MEMBER: "NOT_PROJECT_MEMBER",
16675
- TICKET_NOT_FOUND: "TICKET_NOT_FOUND",
16676
- INVALID_STATUS_TRANSITION: "INVALID_STATUS_TRANSITION",
16677
- STATUS_NOT_FOUND: "STATUS_NOT_FOUND",
16678
- COMMENT_NOT_FOUND: "COMMENT_NOT_FOUND",
16679
- NOT_COMMENT_CREATOR: "NOT_COMMENT_CREATOR",
16680
- TAG_NOT_FOUND: "TAG_NOT_FOUND",
16681
- TAG_ALREADY_EXISTS: "TAG_ALREADY_EXISTS",
16682
- BATCH_SIZE_EXCEEDED: "BATCH_SIZE_EXCEEDED",
16683
- BATCH_PARTIAL_FAILURE: "BATCH_PARTIAL_FAILURE"
16684
- };
16685
-
16686
- class AppError extends Error {
16687
- code;
16688
- statusCode;
16689
- details;
16690
- isRetryable;
16691
- constructor(code, message, statusCode = 500, details, isRetryable = false) {
16692
- super(message);
16693
- this.name = "AppError";
16694
- this.code = code;
16695
- this.statusCode = statusCode;
16696
- this.details = details;
16697
- this.isRetryable = isRetryable;
16698
- if (Error.captureStackTrace) {
16699
- Error.captureStackTrace(this, this.constructor);
16700
- }
16701
- }
16702
- toJSON() {
16703
- return {
16704
- name: this.name,
16705
- code: this.code,
16706
- message: this.message,
16707
- statusCode: this.statusCode,
16708
- details: this.details,
16709
- isRetryable: this.isRetryable
16710
- };
16711
- }
16712
- }
16713
- class ValidationError extends AppError {
16714
- constructor(message, details) {
16715
- super(ERROR_CODES.VALIDATION_ERROR, message, 400, details);
16716
- }
16717
- }
16718
- class KanboAPIError extends AppError {
16719
- originalError;
16720
- constructor(message, statusCode = 500, originalError, details) {
16721
- const code = KanboAPIError.statusToErrorCode(statusCode);
16722
- const isRetryable = KanboAPIError.isStatusRetryable(statusCode);
16723
- super(code, message, statusCode, details, isRetryable);
16724
- this.name = "KanboAPIError";
16725
- this.originalError = originalError;
16726
- }
16727
- static statusToErrorCode(status) {
16728
- switch (status) {
16729
- case 400:
16730
- return ERROR_CODES.BAD_REQUEST;
16731
- case 401:
16732
- return ERROR_CODES.UNAUTHORIZED;
16733
- case 403:
16734
- return ERROR_CODES.FORBIDDEN;
16735
- case 404:
16736
- return ERROR_CODES.NOT_FOUND;
16737
- case 409:
16738
- return ERROR_CODES.CONFLICT;
16739
- case 422:
16740
- return ERROR_CODES.VALIDATION_ERROR;
16741
- case 429:
16742
- return ERROR_CODES.API_RATE_LIMITED;
16743
- case 503:
16744
- return ERROR_CODES.API_UNAVAILABLE;
16745
- default:
16746
- return ERROR_CODES.API_ERROR;
16747
- }
16748
- }
16749
- static isStatusRetryable(status) {
16750
- return status >= 500 || status === 429;
16751
- }
16752
- }
16753
-
16754
- class APITimeoutError extends AppError {
16755
- constructor(timeoutMs) {
16756
- super(ERROR_CODES.API_TIMEOUT, `API request timed out after ${timeoutMs}ms`, 408, { timeoutMs }, true);
16757
- }
16758
- }
16759
-
16760
- class APINotConfiguredError extends AppError {
16761
- constructor(message = "API client not configured") {
16762
- super(ERROR_CODES.API_NOT_CONFIGURED, message, 500);
16763
- }
16764
- }
16765
- class InvalidAPIKeyError extends AppError {
16766
- constructor(message = 'Invalid API key. Keys should start with "kanbo_pat_".') {
16767
- super(ERROR_CODES.INVALID_API_KEY, message, 401);
16768
- }
16769
- }
16770
- class APIKeyRevokedError extends AppError {
16771
- constructor() {
16772
- super(ERROR_CODES.API_KEY_REVOKED, "This API key has been revoked. Please generate a new key from the Kanbo web app.", 401);
16773
- }
16774
- }
16775
-
16776
- class APIKeyExpiredError extends AppError {
16777
- constructor() {
16778
- super(ERROR_CODES.API_KEY_EXPIRED, "This API key has expired. Please generate a new key from the Kanbo web app.", 401);
16779
- }
16780
- }
16781
- function isAppError(error) {
16782
- return error instanceof AppError;
16783
- }
16784
- function isKanboAPIError(error) {
16785
- return error instanceof KanboAPIError;
16786
- }
16787
- function getErrorCode(error) {
16788
- if (isAppError(error)) {
16789
- return error.code;
16790
- }
16791
- return ERROR_CODES.INTERNAL_ERROR;
16792
- }
16793
-
16794
- // src/config/constants.ts
16795
- function loadConfigFile() {
16796
- try {
16797
- const configPath = path.join(os.homedir(), ".kanbo", "config.json");
16798
- if (!fs.existsSync(configPath)) {
16799
- return null;
16800
- }
16801
- const content = fs.readFileSync(configPath, "utf-8");
16802
- const config = JSON.parse(content);
16803
- if (config.version !== 1 || !config.apiKey) {
16804
- return null;
16805
- }
16806
- return config;
16807
- } catch {
16808
- return null;
16809
- }
16810
- }
16811
- function getApiKey() {
16812
- if (process.env.KANBO_API_KEY) {
16813
- return process.env.KANBO_API_KEY;
16814
- }
16815
- const config = loadConfigFile();
16816
- return config?.apiKey;
16817
- }
16818
- function getApiUrl() {
16819
- if (process.env.KANBO_API_URL) {
16820
- return process.env.KANBO_API_URL;
16821
- }
16822
- const config = loadConfigFile();
16823
- return config?.apiUrl || "https://api.kanbo.dev";
16824
- }
16825
- function getOrgId() {
16826
- if (process.env.KANBO_ORG_ID) {
16827
- return process.env.KANBO_ORG_ID;
16828
- }
16829
- const config = loadConfigFile();
16830
- return config?.orgId;
16831
- }
16832
- var CONFIG = {
16833
- KANBO_API_URL: getApiUrl(),
16834
- KANBO_API_KEY: getApiKey(),
16835
- TRANSPORT: process.env.TRANSPORT || "stdio",
16836
- PORT: parseInt(process.env.PORT || "8081", 10),
16837
- KANBO_ORG_ID: getOrgId(),
16838
- USE_OIDC_AUTH: process.env.USE_OIDC_AUTH === "true"
16839
- };
16840
- var SERVER_INFO = {
16841
- name: "kanbo-mcp",
16842
- version: "1.1.5",
16843
- description: "MCP server for Kanbo project management"
16844
- };
16845
- var PAT_CONFIG = {
16846
- PREFIX: "kanbo_pat_",
16847
- TOKEN_LENGTH: 58
16848
- };
16849
- var LIMITS = {
16850
- TITLE_MIN: 1,
16851
- TITLE_MAX: 200,
16852
- DESCRIPTION_MAX: 5000,
16853
- ACCEPTANCE_CRITERIA_MAX: 1e4,
16854
- COMMENT_MAX: 2000,
16855
- NAME_MIN: 1,
16856
- NAME_MAX: 100,
16857
- TAG_NAME_MAX: 30,
16858
- PAGE_SIZE_DEFAULT: 20,
16859
- PAGE_SIZE_MAX: 100,
16860
- BATCH_SIZE_MAX: 50
16861
- };
16862
- var TICKET_LOCATIONS = ["board", "backlog", "completed", "abandoned"];
16863
- var TICKET_PRIORITIES = ["critical", "high", "medium", "low"];
16864
- var TICKET_SIZES = ["xs", "s", "m", "l", "xl"];
16865
- var TAG_COLORS = [
16866
- "gray",
16867
- "sky",
16868
- "blue",
16869
- "indigo",
16870
- "emerald",
16871
- "green",
16872
- "teal",
16873
- "cyan",
16874
- "amber",
16875
- "yellow",
16876
- "orange",
16877
- "red",
16878
- "violet",
16879
- "purple",
16880
- "fuchsia",
16881
- "pink"
16882
- ];
16936
+ // src/index.ts
16937
+ init_constants();
16883
16938
 
16884
16939
  // node_modules/zod/v4/core/core.js
16885
16940
  var NEVER = Object.freeze({
@@ -24020,9 +24075,18 @@ class StdioServerTransport {
24020
24075
  }
24021
24076
  }
24022
24077
 
24078
+ // src/server.ts
24079
+ init_constants();
24080
+
24023
24081
  // src/lib/client/index.ts
24082
+ init_constants();
24083
+ init_errors();
24024
24084
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
24025
24085
 
24086
+ // src/lib/client/core.ts
24087
+ init_errors();
24088
+ init_constants();
24089
+
24026
24090
  // src/config/endpoints.ts
24027
24091
  var EP = {
24028
24092
  AUTH: {
@@ -24207,7 +24271,7 @@ var baseLogger = import_pino.default({
24207
24271
  name: config2.appName,
24208
24272
  level: config2.level,
24209
24273
  redact: {
24210
- paths: REDACT_PATHS.map((p) => `*.${p}`),
24274
+ paths: REDACT_PATHS.flatMap((p) => [p, `*.${p}`]),
24211
24275
  censor: "[REDACTED]"
24212
24276
  },
24213
24277
  serializers: {
@@ -24218,12 +24282,8 @@ var baseLogger = import_pino.default({
24218
24282
  base: {
24219
24283
  service: config2.appName,
24220
24284
  env: "development"
24221
- },
24222
- transport: {
24223
- target: "pino/file",
24224
- options: { destination: 2 }
24225
24285
  }
24226
- });
24286
+ }, import_pino.default.destination(2));
24227
24287
  function createLogger(bindings = {}) {
24228
24288
  const boundLogger = Object.keys(bindings).length > 0 ? baseLogger.child(bindings) : baseLogger;
24229
24289
  const createBoundLogFn = (level) => {
@@ -24329,6 +24389,8 @@ class KanboClient {
24329
24389
  orgId;
24330
24390
  useOidcAuth;
24331
24391
  authContext;
24392
+ authContextCachedAt;
24393
+ static AUTH_CONTEXT_TTL_MS = 60 * 60 * 1000;
24332
24394
  authContextPromise;
24333
24395
  constructor(config3) {
24334
24396
  this.apiUrl = config3.apiUrl.replace(/\/$/, "");
@@ -24352,7 +24414,7 @@ class KanboClient {
24352
24414
  return this._request(method, path2, body, options);
24353
24415
  }
24354
24416
  async _request(method, path2, body, options) {
24355
- if (path2.includes("..") || path2.includes("%2F") || path2.includes("%2f") || path2.includes("%00")) {
24417
+ if (path2.includes("..") || path2.includes("%2F") || path2.includes("%2f") || path2.includes("%00") || path2.includes("%25")) {
24356
24418
  throw new ValidationError(`Invalid path: contains forbidden characters`);
24357
24419
  }
24358
24420
  let url = `${this.apiUrl}/api${path2}`;
@@ -24468,8 +24530,13 @@ class KanboClient {
24468
24530
  return this.orgId;
24469
24531
  }
24470
24532
  async validateAndGetContext() {
24471
- if (this.authContext) {
24472
- return this.authContext;
24533
+ if (this.authContext && this.authContextCachedAt) {
24534
+ const age = Date.now() - this.authContextCachedAt;
24535
+ if (age < KanboClient.AUTH_CONTEXT_TTL_MS) {
24536
+ return this.authContext;
24537
+ }
24538
+ this.authContext = undefined;
24539
+ this.authContextCachedAt = undefined;
24473
24540
  }
24474
24541
  if (this.authContextPromise) {
24475
24542
  return this.authContextPromise;
@@ -24477,6 +24544,7 @@ class KanboClient {
24477
24544
  this.authContextPromise = this.fetchAuthContext();
24478
24545
  try {
24479
24546
  const context = await this.authContextPromise;
24547
+ this.authContextPromise = undefined;
24480
24548
  return context;
24481
24549
  } catch (error2) {
24482
24550
  this.authContextPromise = undefined;
@@ -24504,6 +24572,7 @@ class KanboClient {
24504
24572
  organizations: org ? [org] : [],
24505
24573
  currentOrg: org || undefined
24506
24574
  };
24575
+ this.authContextCachedAt = Date.now();
24507
24576
  if (!this.orgId && this.authContext.organizations.length > 0) {
24508
24577
  this.orgId = this.authContext.organizations[0].id;
24509
24578
  }
@@ -24581,6 +24650,7 @@ KanboClient.prototype.removeProjectMember = async function(projectId, userId) {
24581
24650
  };
24582
24651
 
24583
24652
  // src/lib/client/tickets.ts
24653
+ init_errors();
24584
24654
  KanboClient.prototype.listTickets = async function(projectId, params) {
24585
24655
  return this.get(EP.TICKETS.LIST(projectId), params);
24586
24656
  };
@@ -24591,17 +24661,19 @@ KanboClient.prototype.getMyTickets = async function(projectId, params) {
24591
24661
  return this.listTickets(projectId, { ...params, assigneeId });
24592
24662
  }
24593
24663
  const projects = await this.listProjects();
24594
- const allTickets = [];
24595
24664
  const perProjectLimit = params?.limit ?? 20;
24596
- for (const project of projects.data) {
24597
- try {
24598
- const result = await this.listTickets(project.id, {
24599
- ...params,
24600
- assigneeId,
24601
- limit: perProjectLimit
24602
- });
24603
- allTickets.push(...result.data);
24604
- } catch {}
24665
+ const results = await Promise.allSettled(projects.data.map((project) => this.listTickets(project.id, {
24666
+ ...params,
24667
+ assigneeId,
24668
+ limit: perProjectLimit
24669
+ })));
24670
+ const allTickets = [];
24671
+ for (const result of results) {
24672
+ if (result.status === "fulfilled") {
24673
+ allTickets.push(...result.value.data);
24674
+ } else if (isKanboAPIError(result.reason) && result.reason.statusCode === 401) {
24675
+ throw result.reason;
24676
+ }
24605
24677
  }
24606
24678
  return { data: allTickets };
24607
24679
  };
@@ -24974,9 +25046,12 @@ function getKanboClient() {
24974
25046
  if (requestClient) {
24975
25047
  return requestClient;
24976
25048
  }
25049
+ if (CONFIG.TRANSPORT === "http") {
25050
+ throw new APINotConfiguredError("No request-scoped client available. In HTTP mode, all tool calls must run within runWithClient().");
25051
+ }
24977
25052
  if (!clientInstance) {
24978
25053
  if (!CONFIG.KANBO_API_KEY) {
24979
- throw new APINotConfiguredError("No API key available. In HTTP mode, include Authorization header. " + "In stdio mode, set KANBO_API_KEY environment variable.");
25054
+ throw new APINotConfiguredError('No API key available. Set KANBO_API_KEY environment variable or run "kanbo-mcp login".');
24980
25055
  }
24981
25056
  clientInstance = new KanboClient({
24982
25057
  apiKey: CONFIG.KANBO_API_KEY,
@@ -24986,7 +25061,11 @@ function getKanboClient() {
24986
25061
  }
24987
25062
  return clientInstance;
24988
25063
  }
25064
+ // src/server.ts
25065
+ init_errors();
25066
+
24989
25067
  // src/tools/types.ts
25068
+ init_errors();
24990
25069
  function successResult(data) {
24991
25070
  return { success: true, data };
24992
25071
  }
@@ -25007,6 +25086,47 @@ function errorResultFromError(error2) {
25007
25086
  const details = isAppError(error2) ? error2.details : undefined;
25008
25087
  return errorResult(code, message, details);
25009
25088
  }
25089
+ var JSON_TYPE_CHECKS = {
25090
+ string: (v) => typeof v === "string",
25091
+ number: (v) => typeof v === "number",
25092
+ integer: (v) => typeof v === "number" && Number.isInteger(v),
25093
+ boolean: (v) => typeof v === "boolean",
25094
+ array: (v) => Array.isArray(v),
25095
+ object: (v) => typeof v === "object" && v !== null && !Array.isArray(v)
25096
+ };
25097
+ function validateToolArgs(args, schema) {
25098
+ if (!schema || schema.type !== "object")
25099
+ return null;
25100
+ const properties = schema.properties ?? {};
25101
+ const required2 = schema.required ?? [];
25102
+ for (const key of required2) {
25103
+ if (args[key] === undefined || args[key] === null) {
25104
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, `Missing required argument: ${key}`);
25105
+ }
25106
+ }
25107
+ for (const [key, value] of Object.entries(args)) {
25108
+ if (value === undefined || value === null)
25109
+ continue;
25110
+ const prop = properties[key];
25111
+ if (!prop?.type)
25112
+ continue;
25113
+ const checker = JSON_TYPE_CHECKS[prop.type];
25114
+ if (checker && !checker(value)) {
25115
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, `Argument '${key}' must be of type ${prop.type}, got ${typeof value}`);
25116
+ }
25117
+ }
25118
+ return null;
25119
+ }
25120
+ function withArgValidation(tool, handler) {
25121
+ if (!tool.inputSchema)
25122
+ return handler;
25123
+ return async (args) => {
25124
+ const validationError = validateToolArgs(args, tool.inputSchema);
25125
+ if (validationError)
25126
+ return validationError;
25127
+ return handler(args);
25128
+ };
25129
+ }
25010
25130
  function formatToolResult(result) {
25011
25131
  return JSON.stringify(result, null, 2);
25012
25132
  }
@@ -25339,6 +25459,8 @@ var projectTools = [
25339
25459
  ];
25340
25460
 
25341
25461
  // src/tools/tickets.ts
25462
+ init_errors();
25463
+ init_constants();
25342
25464
  var listTicketsTool = {
25343
25465
  tool: {
25344
25466
  name: "list_tickets",
@@ -25457,6 +25579,12 @@ var getTicketByKeyTool = {
25457
25579
  const client = getKanboClient();
25458
25580
  const authContext = client.getAuthContext();
25459
25581
  let orgSlug = args.orgSlug;
25582
+ if (orgSlug && authContext?.organizations?.length) {
25583
+ const isAuthorized = authContext.organizations.some((o) => o.slug === orgSlug);
25584
+ if (!isAuthorized) {
25585
+ return errorResult(ERROR_CODES.NO_ORG_SELECTED, "You do not have access to the specified organization.");
25586
+ }
25587
+ }
25460
25588
  if (!orgSlug && authContext?.currentOrg) {
25461
25589
  orgSlug = authContext.currentOrg.slug;
25462
25590
  }
@@ -26658,6 +26786,7 @@ var workflowTools = [
26658
26786
  ];
26659
26787
 
26660
26788
  // src/tools/comments.ts
26789
+ init_constants();
26661
26790
  var listCommentsTool = {
26662
26791
  tool: {
26663
26792
  name: "list_comments",
@@ -26837,6 +26966,7 @@ var commentTools = [
26837
26966
  ];
26838
26967
 
26839
26968
  // src/tools/tags.ts
26969
+ init_constants();
26840
26970
  var listTagsTool = {
26841
26971
  tool: {
26842
26972
  name: "list_tags",
@@ -28823,6 +28953,7 @@ var usageTools = [
28823
28953
  ];
28824
28954
 
28825
28955
  // src/tools/organization.ts
28956
+ init_errors();
28826
28957
  var listOrganizationsTool = {
28827
28958
  tool: {
28828
28959
  name: "list_organizations",
@@ -29051,6 +29182,8 @@ var watcherTools = [
29051
29182
  ];
29052
29183
 
29053
29184
  // src/tools/batch.ts
29185
+ init_errors();
29186
+ init_constants();
29054
29187
  var BATCH_CONCURRENCY_LIMIT = 5;
29055
29188
  async function mapWithConcurrency(items, fn, concurrency = BATCH_CONCURRENCY_LIMIT) {
29056
29189
  const results = new Array(items.length);
@@ -29527,8 +29660,33 @@ var TOOL_INDEX = {
29527
29660
  "Usage & Billing": {
29528
29661
  tools: ["get_mcp_usage", "get_ai_usage"],
29529
29662
  description: "Check MCP request and AI token quotas."
29663
+ },
29664
+ Git: {
29665
+ tools: ["get_commit_prefix"],
29666
+ description: "Git commit conventions. Use get_commit_prefix to generate properly formatted commit message prefixes from ticket keys."
29530
29667
  }
29531
29668
  },
29669
+ git_conventions: {
29670
+ description: "IMPORTANT: Follow these conventions when committing code related to Kanbo tickets. This ensures commits appear in the Git tab of each ticket in the Kanbo UI.",
29671
+ commit_message_format: {
29672
+ single_ticket: "<TICKET_KEY>: <description>",
29673
+ multiple_tickets: "<TICKET_KEY_1> <TICKET_KEY_2>: <description>",
29674
+ examples: [
29675
+ "WHLZ-59: implement Google OAuth login flow",
29676
+ "WHLZ-59 WHLZ-60: add shared auth middleware for OAuth providers",
29677
+ "UNO-12: fix pagination bug in ticket list"
29678
+ ]
29679
+ },
29680
+ rules: [
29681
+ "ALWAYS prefix commit messages with the ticket key(s) you are working on",
29682
+ "The ticket key format is PROJECT_PREFIX-NUMBER (e.g., WHLZ-59, UNO-12)",
29683
+ "Use get_commit_prefix tool to generate the correct prefix for one or more tickets",
29684
+ "The prefix links commits to tickets in the Kanbo UI Git tab — without it, commits are invisible to the ticket",
29685
+ 'Use lowercase description after the prefix, starting with a verb (e.g., "add", "fix", "update", "refactor")',
29686
+ "Keep the first line under 72 characters"
29687
+ ],
29688
+ workflow: "get_ticket or get_ticket_by_key (note the ticket key) → do the work → get_commit_prefix(ticketKeys) → git commit with the returned prefix"
29689
+ },
29532
29690
  common_workflows: {
29533
29691
  "Create a high-quality ticket": "list_statuses + list_priorities + list_sizes + list_ticket_types + list_tags (cache IDs) → find_similar_tickets (ALWAYS check for duplicates/related tickets — link via parentId if related) → create_ticket (with description, acceptanceCriteria [REQUIRED], typeSlug) → set_ticket_tags (add 2-5 tags)",
29534
29692
  "AI-assisted ticket creation": "find_similar_tickets (check duplicates/related first) → check_clarity → polish_title → generate_description → suggest_attributes → create_ticket (ALWAYS include acceptanceCriteria + parentId if related ticket found) → set_ticket_tags",
@@ -29537,7 +29695,8 @@ var TOOL_INDEX = {
29537
29695
  "Daily standup": "get_my_tickets + get_overdue_tickets + get_tickets_due_soon",
29538
29696
  "Release shipping": "get_release_tickets (verify all complete) → mark_release_shipped",
29539
29697
  "Bulk cleanup": "list_tickets (filter) → batch_complete_tickets or batch_move_tickets",
29540
- "Team workload review": "get_member_analytics + get_distribution_analytics"
29698
+ "Team workload review": "get_member_analytics + get_distribution_analytics",
29699
+ "Committing code for a ticket": 'get_ticket_by_key (note ticket key) → implement changes → get_commit_prefix(ticketKeys: ["PROJ-123"]) → git commit -m "<prefix> <description>"'
29541
29700
  }
29542
29701
  };
29543
29702
  var helpTools = [
@@ -29576,6 +29735,54 @@ var helpTools = [
29576
29735
  }
29577
29736
  }
29578
29737
  ];
29738
+
29739
+ // src/tools/git.ts
29740
+ init_errors();
29741
+ var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
29742
+ var getCommitPrefixTool = {
29743
+ tool: {
29744
+ name: "get_commit_prefix",
29745
+ description: "Generate a properly formatted git commit message prefix from one or more Kanbo ticket keys. " + "IMPORTANT: Always use this before committing code related to Kanbo tickets. " + "The prefix ensures commits appear in the Git tab of each ticket in the Kanbo UI. " + "Without the prefix, commits will not be linked to tickets.",
29746
+ inputSchema: {
29747
+ type: "object",
29748
+ properties: {
29749
+ ticketKeys: {
29750
+ type: "array",
29751
+ items: { type: "string" },
29752
+ description: 'One or more ticket keys (e.g., ["WHLZ-59"] or ["WHLZ-59", "WHLZ-60"]). ' + "Use the ticket key format: PROJECT_PREFIX-NUMBER."
29753
+ }
29754
+ },
29755
+ required: ["ticketKeys"]
29756
+ }
29757
+ },
29758
+ handler: async (args) => {
29759
+ const ticketKeys = args.ticketKeys;
29760
+ if (!Array.isArray(ticketKeys) || ticketKeys.length === 0) {
29761
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, 'ticketKeys must be a non-empty array of ticket keys (e.g., ["WHLZ-59"])');
29762
+ }
29763
+ const invalidKeys = ticketKeys.filter((key) => !TICKET_KEY_PATTERN.test(key));
29764
+ if (invalidKeys.length > 0) {
29765
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, `Invalid ticket key format: ${invalidKeys.join(", ")}. Expected format: PROJECT_PREFIX-NUMBER (e.g., WHLZ-59, UNO-12)`);
29766
+ }
29767
+ const uniqueKeys = [...new Set(ticketKeys)];
29768
+ const prefix = uniqueKeys.join(" ");
29769
+ return successResult({
29770
+ prefix: `${prefix}:`,
29771
+ example: `${prefix}: <describe your change>`,
29772
+ format_rules: [
29773
+ "Use lowercase description after the prefix",
29774
+ "Start with a verb: add, fix, update, refactor, remove, implement",
29775
+ "Keep the first line under 72 characters"
29776
+ ],
29777
+ examples: [
29778
+ `${prefix}: implement the feature`,
29779
+ `${prefix}: fix validation bug`,
29780
+ `${prefix}: update error handling`
29781
+ ]
29782
+ });
29783
+ }
29784
+ };
29785
+ var gitTools = [getCommitPrefixTool];
29579
29786
  // src/tools/index.ts
29580
29787
  var allToolDefinitions = [
29581
29788
  ...helpTools,
@@ -29597,7 +29804,8 @@ var allToolDefinitions = [
29597
29804
  ...analyticsTools,
29598
29805
  ...notificationTools,
29599
29806
  ...aiTools,
29600
- ...usageTools
29807
+ ...usageTools,
29808
+ ...gitTools
29601
29809
  ];
29602
29810
 
29603
29811
  class ToolRegistryImpl {
@@ -29610,7 +29818,7 @@ class ToolRegistryImpl {
29610
29818
  }
29611
29819
  register(definition) {
29612
29820
  this.tools.push(definition.tool);
29613
- this.handlers.set(definition.tool.name, definition.handler);
29821
+ this.handlers.set(definition.tool.name, withArgValidation(definition.tool, definition.handler));
29614
29822
  }
29615
29823
  getTool(name) {
29616
29824
  return this.tools.find((t) => t.name === name);
@@ -43942,6 +44150,9 @@ var _Elysia = class _Elysia2 {
43942
44150
  };
43943
44151
  var Elysia = _Elysia;
43944
44152
 
44153
+ // src/server-http.ts
44154
+ init_constants();
44155
+
43945
44156
  // src/config/routes.ts
43946
44157
  var ROUTES = {
43947
44158
  HEALTH: {
@@ -43959,6 +44170,7 @@ var ROUTES = {
43959
44170
  };
43960
44171
 
43961
44172
  // src/server-http.ts
44173
+ init_errors();
43962
44174
  var httpLogger = logger.withCategory(LogCategory.SYSTEM);
43963
44175
  function extractApiKey(request) {
43964
44176
  const apiKeyHeader = request.headers.get("x-kanbo-api-key");
@@ -43977,6 +44189,12 @@ function extractApiKey(request) {
43977
44189
  }
43978
44190
  return null;
43979
44191
  }
44192
+ function jsonErrorResponse(status2, code, message) {
44193
+ return new Response(JSON.stringify({
44194
+ success: false,
44195
+ error: { code, message, timestamp: new Date().toISOString() }
44196
+ }), { status: status2, headers: { "Content-Type": "application/json" } });
44197
+ }
43980
44198
  function unauthorizedResponse(message) {
43981
44199
  return new Response(JSON.stringify({
43982
44200
  success: false,
@@ -43992,7 +44210,36 @@ function unauthorizedResponse(message) {
43992
44210
  }
43993
44211
  async function startHttpServer() {
43994
44212
  httpLogger.info("Starting in HTTP mode with per-request authentication");
43995
- const app = new Elysia().get(ROUTES.HEALTH.INDEX, () => ({
44213
+ const app = new Elysia().onRequest(({ request }) => {
44214
+ const origin = request.headers.get("origin");
44215
+ if (request.method === "OPTIONS") {
44216
+ const headers = {
44217
+ "Access-Control-Max-Age": "86400",
44218
+ "Access-Control-Allow-Methods": "GET, POST",
44219
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Kanbo-API-Key"
44220
+ };
44221
+ if (origin && CONFIG.CORS_ORIGINS.includes(origin)) {
44222
+ headers["Access-Control-Allow-Origin"] = origin;
44223
+ headers["Vary"] = "Origin";
44224
+ }
44225
+ return new Response(null, { status: 204, headers });
44226
+ }
44227
+ }).onAfterHandle(({ request, response }) => {
44228
+ const origin = request.headers.get("origin");
44229
+ if (origin && CONFIG.CORS_ORIGINS.includes(origin) && response instanceof Response) {
44230
+ response.headers.set("Access-Control-Allow-Origin", origin);
44231
+ response.headers.set("Vary", "Origin");
44232
+ }
44233
+ }).onParse(({ request }) => {
44234
+ const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
44235
+ if (contentLength > CONFIG.MAX_BODY_SIZE) {
44236
+ throw new Error(`PAYLOAD_TOO_LARGE`);
44237
+ }
44238
+ }).onError(({ error: error2 }) => {
44239
+ if (error2.message === "PAYLOAD_TOO_LARGE") {
44240
+ return jsonErrorResponse(413, ERROR_CODES.VALIDATION_ERROR, `Request body too large. Maximum size is ${CONFIG.MAX_BODY_SIZE} bytes.`);
44241
+ }
44242
+ }).get(ROUTES.HEALTH.INDEX, () => ({
43996
44243
  status: "ok",
43997
44244
  server: SERVER_INFO.name,
43998
44245
  version: SERVER_INFO.version
@@ -0,0 +1,139 @@
1
+ # Git Commit Conventions
2
+
3
+ > **Purpose**: Ensures git commits are linked to Kanbo tickets and appear in the **Git tab** of the Kanbo UI. Any AI agent using the Kanbo MCP server should follow these conventions when committing code.
4
+ >
5
+ > **Last Updated**: 2026-03-12
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ Kanbo's Git tab tracks commits associated with a ticket by matching the **ticket key prefix** in commit messages. When a commit message starts with a ticket key (e.g., `WHLZ-59:`), that commit is automatically linked to the ticket and displayed in its Git tab.
12
+
13
+ Without the correct prefix, commits are **invisible** to the ticket — there is no way to retroactively link them.
14
+
15
+ ---
16
+
17
+ ## Commit Message Format
18
+
19
+ ### Single Ticket
20
+
21
+ ```
22
+ <TICKET_KEY>: <description>
23
+ ```
24
+
25
+ **Examples:**
26
+
27
+ ```
28
+ WHLZ-59: implement Google OAuth login flow
29
+ UNO-12: fix pagination bug in ticket list
30
+ KANBO-7: add rate limiting middleware
31
+ ```
32
+
33
+ ### Multiple Tickets
34
+
35
+ When a single commit applies to more than one ticket, include all keys separated by spaces:
36
+
37
+ ```
38
+ <TICKET_KEY_1> <TICKET_KEY_2>: <description>
39
+ ```
40
+
41
+ **Examples:**
42
+
43
+ ```
44
+ WHLZ-59 WHLZ-60: add shared auth middleware for OAuth providers
45
+ UNO-12 UNO-15: refactor validation logic for both endpoints
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Rules
51
+
52
+ | Rule | Details |
53
+ |------|---------|
54
+ | **Always prefix** | Every commit related to a Kanbo ticket MUST start with the ticket key(s) |
55
+ | **Key format** | `PROJECT_PREFIX-NUMBER` — uppercase letters followed by a dash and number (e.g., `WHLZ-59`) |
56
+ | **Description style** | Lowercase, starting with a verb (`add`, `fix`, `update`, `refactor`, `remove`, `implement`) |
57
+ | **Line length** | Keep the first line under 72 characters |
58
+ | **No prefix = no link** | Commits without the ticket key prefix will NOT appear in the Kanbo Git tab |
59
+
60
+ ---
61
+
62
+ ## MCP Tool: `get_commit_prefix`
63
+
64
+ The Kanbo MCP server provides a `get_commit_prefix` tool that generates properly formatted commit message prefixes.
65
+
66
+ ### Usage
67
+
68
+ ```json
69
+ {
70
+ "tool": "get_commit_prefix",
71
+ "arguments": {
72
+ "ticketKeys": ["WHLZ-59"]
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Response
78
+
79
+ ```json
80
+ {
81
+ "success": true,
82
+ "data": {
83
+ "prefix": "WHLZ-59:",
84
+ "example": "WHLZ-59: <describe your change>",
85
+ "format_rules": [
86
+ "Use lowercase description after the prefix",
87
+ "Start with a verb: add, fix, update, refactor, remove, implement",
88
+ "Keep the first line under 72 characters"
89
+ ],
90
+ "examples": [
91
+ "WHLZ-59: implement the feature",
92
+ "WHLZ-59: fix validation bug",
93
+ "WHLZ-59: update error handling"
94
+ ]
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Multiple Tickets
100
+
101
+ ```json
102
+ {
103
+ "tool": "get_commit_prefix",
104
+ "arguments": {
105
+ "ticketKeys": ["WHLZ-59", "WHLZ-60"]
106
+ }
107
+ }
108
+ ```
109
+
110
+ Returns prefix: `WHLZ-59 WHLZ-60:`
111
+
112
+ ### Validation
113
+
114
+ The tool validates that each key matches the expected format (`PROJECT_PREFIX-NUMBER`). Invalid keys like `whlz-59`, `WHLZ59`, or `59` will return an error with guidance on the correct format.
115
+
116
+ ---
117
+
118
+ ## Agent Workflow
119
+
120
+ The recommended workflow for an AI agent working on a Kanbo ticket:
121
+
122
+ ```
123
+ 1. get_ticket_by_key("WHLZ-59") — Retrieve ticket details
124
+ 2. Implement the changes — Write code
125
+ 3. get_commit_prefix(["WHLZ-59"]) — Get the formatted prefix
126
+ 4. git commit -m "WHLZ-59: ..." — Commit with the prefix
127
+ ```
128
+
129
+ This workflow is also documented in the `get_help` tool under `common_workflows` → "Committing code for a ticket" and in the `git_conventions` section.
130
+
131
+ ---
132
+
133
+ ## Discovery
134
+
135
+ Agents discover these conventions through:
136
+
137
+ 1. **`get_help` tool** — The `git_conventions` section describes the format, rules, and workflow
138
+ 2. **`get_commit_prefix` tool** — The tool description itself explains why the prefix is required
139
+ 3. **`common_workflows`** — The "Committing code for a ticket" workflow guides agents step-by-step
package/docs/SETUP.md CHANGED
@@ -18,7 +18,7 @@ This will:
18
18
  1. Open your browser to sign in with your Kanbo account
19
19
  2. Let you select which organization to use
20
20
  3. Automatically create and store an API key
21
- 4. **Auto-register the MCP server** with detected editors (VS Code, Cursor, Claude Desktop, Claude Code CLI)
21
+ 4. **Auto-register the MCP server** with detected editors (VS Code, VS Code Insiders, Cursor, Windsurf, Claude Code, Claude Desktop, Claude Code CLI)
22
22
 
23
23
  You'll see output like:
24
24
 
@@ -129,7 +129,7 @@ This means you can:
129
129
 
130
130
  ## Using with Other AI Tools
131
131
 
132
- > **Note:** `npx @kanbodev/mcp login` auto-registers with VS Code, Cursor, Claude Desktop, and Claude Code CLI. The manual configs below are only needed if auto-registration didn't detect your editor.
132
+ > **Note:** `npx @kanbodev/mcp login` auto-registers with VS Code, VS Code Insiders, Cursor, Windsurf, Claude Code, Claude Desktop, and Claude Code CLI. The manual configs below are only needed if auto-registration didn't detect your editor.
133
133
 
134
134
  ### Cursor
135
135
 
@@ -151,6 +151,38 @@ Add to `~/.cursor/mcp.json`:
151
151
 
152
152
  See the full [VS Code setup guide](VSCODE.md) for native MCP support (v1.99+), Claude Code extension, and Cursor/Windsurf.
153
153
 
154
+ ### Windsurf
155
+
156
+ Add to `~/.codeium/windsurf/mcp_config.json`:
157
+
158
+ ```json
159
+ {
160
+ "mcpServers": {
161
+ "kanbodev": {
162
+ "type": "stdio",
163
+ "command": "npx",
164
+ "args": ["@kanbodev/mcp"]
165
+ }
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### Claude Code (VS Code Extension)
171
+
172
+ Add to `~/.claude/settings.json`:
173
+
174
+ ```json
175
+ {
176
+ "mcpServers": {
177
+ "kanbodev": {
178
+ "type": "stdio",
179
+ "command": "npx",
180
+ "args": ["@kanbodev/mcp"]
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
154
186
  ### Claude Code CLI
155
187
 
156
188
  ```bash
package/docs/VSCODE.md CHANGED
@@ -94,7 +94,7 @@ Open the terminal in VS Code and run:
94
94
  claude mcp add kanbo -- npx @kanbodev/mcp
95
95
  ```
96
96
 
97
- This writes the config to `~/.claude.json` (global) by default.
97
+ This writes the config to `~/.claude.json` (global) by default. The `login` command also auto-registers in `~/.claude/settings.json`.
98
98
 
99
99
  ### Via config file
100
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanbodev/mcp",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "MCP (Model Context Protocol) server for Kanbo - AI-native project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",