@kanbodev/mcp 1.1.4 → 1.1.6

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/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.6",
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,7 +15886,8 @@ 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
  }
@@ -15652,7 +15900,8 @@ function getClaudeDesktopMcpPath() {
15652
15900
  return path3.join(os3.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
15653
15901
  }
15654
15902
  if (platform === "win32") {
15655
- return path3.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
15903
+ const appData = process.env.APPDATA || path3.join(os3.homedir(), "AppData", "Roaming");
15904
+ return path3.join(appData, "Claude", "claude_desktop_config.json");
15656
15905
  }
15657
15906
  return path3.join(os3.homedir(), ".config", "Claude", "claude_desktop_config.json");
15658
15907
  }
@@ -15692,7 +15941,7 @@ function registerInConfigFile(configPath, hostName, serversKey = "servers") {
15692
15941
  }
15693
15942
  function isClaudeCliAvailable() {
15694
15943
  try {
15695
- execFileSync("claude", ["--version"], { stdio: "pipe" });
15944
+ execFileSync("claude", ["--version"], { stdio: "pipe", ...shellOpt });
15696
15945
  return true;
15697
15946
  } catch {
15698
15947
  return false;
@@ -15705,7 +15954,8 @@ function registerWithClaudeCli() {
15705
15954
  return { host: hostName, success: false, message: "Claude Code CLI not detected" };
15706
15955
  }
15707
15956
  execFileSync("claude", ["mcp", "add", SERVER_NAME, "--", "npx", "@kanbodev/mcp"], {
15708
- stdio: "pipe"
15957
+ stdio: "pipe",
15958
+ ...shellOpt
15709
15959
  });
15710
15960
  return { host: hostName, success: true, message: "Registered via Claude Code CLI" };
15711
15961
  } catch (error2) {
@@ -15769,13 +16019,14 @@ function printManualInstructions() {
15769
16019
  console.log(" Or register via Claude Code CLI:");
15770
16020
  console.log(" claude mcp add kanbodev -- npx @kanbodev/mcp");
15771
16021
  }
15772
- var SERVER_NAME = "kanbodev", MCP_SERVER_ENTRY;
16022
+ var SERVER_NAME = "kanbodev", MCP_SERVER_ENTRY, shellOpt;
15773
16023
  var init_register = __esm(() => {
15774
16024
  MCP_SERVER_ENTRY = {
15775
16025
  type: "stdio",
15776
16026
  command: "npx",
15777
16027
  args: ["@kanbodev/mcp"]
15778
16028
  };
16029
+ shellOpt = process.platform === "win32" ? { shell: true } : {};
15779
16030
  });
15780
16031
 
15781
16032
  // node_modules/is-docker/index.js
@@ -16434,7 +16685,6 @@ __export(exports_login, {
16434
16685
  login: () => login
16435
16686
  });
16436
16687
  import * as crypto2 from "node:crypto";
16437
- import { createRequire as createRequire2 } from "node:module";
16438
16688
  async function login() {
16439
16689
  console.log(`
16440
16690
  Kanbo CLI Login
@@ -16520,16 +16770,16 @@ async function login() {
16520
16770
  printRegistrationResults(registrationResults);
16521
16771
  console.log("");
16522
16772
  }
16523
- var KANBO_WEB_URL, DEFAULT_API_URL, PORT_RANGE, require2, CLI_VERSION;
16773
+ var KANBO_WEB_URL, DEFAULT_API_URL, PORT_RANGE, CLI_VERSION;
16524
16774
  var init_login = __esm(() => {
16525
16775
  init_callback_server();
16526
16776
  init_config();
16527
16777
  init_register();
16778
+ init_constants();
16528
16779
  KANBO_WEB_URL = process.env.KANBO_WEB_URL || "https://kanbo.dev";
16529
16780
  DEFAULT_API_URL = process.env.KANBO_API_URL || "https://api.kanbo.dev";
16530
16781
  PORT_RANGE = { start: 9876, end: 9899 };
16531
- require2 = createRequire2(import.meta.url);
16532
- CLI_VERSION = require2("../../package.json").version;
16782
+ CLI_VERSION = SERVER_INFO.version;
16533
16783
  });
16534
16784
 
16535
16785
  // src/cli/logout.ts
@@ -16643,243 +16893,8 @@ var init_install = __esm(() => {
16643
16893
  init_register();
16644
16894
  });
16645
16895
 
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.4",
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
- ];
16896
+ // src/index.ts
16897
+ init_constants();
16883
16898
 
16884
16899
  // node_modules/zod/v4/core/core.js
16885
16900
  var NEVER = Object.freeze({
@@ -24020,9 +24035,18 @@ class StdioServerTransport {
24020
24035
  }
24021
24036
  }
24022
24037
 
24038
+ // src/server.ts
24039
+ init_constants();
24040
+
24023
24041
  // src/lib/client/index.ts
24042
+ init_constants();
24043
+ init_errors();
24024
24044
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
24025
24045
 
24046
+ // src/lib/client/core.ts
24047
+ init_errors();
24048
+ init_constants();
24049
+
24026
24050
  // src/config/endpoints.ts
24027
24051
  var EP = {
24028
24052
  AUTH: {
@@ -24207,7 +24231,7 @@ var baseLogger = import_pino.default({
24207
24231
  name: config2.appName,
24208
24232
  level: config2.level,
24209
24233
  redact: {
24210
- paths: REDACT_PATHS.map((p) => `*.${p}`),
24234
+ paths: REDACT_PATHS.flatMap((p) => [p, `*.${p}`]),
24211
24235
  censor: "[REDACTED]"
24212
24236
  },
24213
24237
  serializers: {
@@ -24218,12 +24242,8 @@ var baseLogger = import_pino.default({
24218
24242
  base: {
24219
24243
  service: config2.appName,
24220
24244
  env: "development"
24221
- },
24222
- transport: {
24223
- target: "pino/file",
24224
- options: { destination: 2 }
24225
24245
  }
24226
- });
24246
+ }, import_pino.default.destination(2));
24227
24247
  function createLogger(bindings = {}) {
24228
24248
  const boundLogger = Object.keys(bindings).length > 0 ? baseLogger.child(bindings) : baseLogger;
24229
24249
  const createBoundLogFn = (level) => {
@@ -24329,6 +24349,8 @@ class KanboClient {
24329
24349
  orgId;
24330
24350
  useOidcAuth;
24331
24351
  authContext;
24352
+ authContextCachedAt;
24353
+ static AUTH_CONTEXT_TTL_MS = 60 * 60 * 1000;
24332
24354
  authContextPromise;
24333
24355
  constructor(config3) {
24334
24356
  this.apiUrl = config3.apiUrl.replace(/\/$/, "");
@@ -24352,7 +24374,7 @@ class KanboClient {
24352
24374
  return this._request(method, path2, body, options);
24353
24375
  }
24354
24376
  async _request(method, path2, body, options) {
24355
- if (path2.includes("..") || path2.includes("%2F") || path2.includes("%2f") || path2.includes("%00")) {
24377
+ if (path2.includes("..") || path2.includes("%2F") || path2.includes("%2f") || path2.includes("%00") || path2.includes("%25")) {
24356
24378
  throw new ValidationError(`Invalid path: contains forbidden characters`);
24357
24379
  }
24358
24380
  let url = `${this.apiUrl}/api${path2}`;
@@ -24468,8 +24490,13 @@ class KanboClient {
24468
24490
  return this.orgId;
24469
24491
  }
24470
24492
  async validateAndGetContext() {
24471
- if (this.authContext) {
24472
- return this.authContext;
24493
+ if (this.authContext && this.authContextCachedAt) {
24494
+ const age = Date.now() - this.authContextCachedAt;
24495
+ if (age < KanboClient.AUTH_CONTEXT_TTL_MS) {
24496
+ return this.authContext;
24497
+ }
24498
+ this.authContext = undefined;
24499
+ this.authContextCachedAt = undefined;
24473
24500
  }
24474
24501
  if (this.authContextPromise) {
24475
24502
  return this.authContextPromise;
@@ -24477,6 +24504,7 @@ class KanboClient {
24477
24504
  this.authContextPromise = this.fetchAuthContext();
24478
24505
  try {
24479
24506
  const context = await this.authContextPromise;
24507
+ this.authContextPromise = undefined;
24480
24508
  return context;
24481
24509
  } catch (error2) {
24482
24510
  this.authContextPromise = undefined;
@@ -24504,6 +24532,7 @@ class KanboClient {
24504
24532
  organizations: org ? [org] : [],
24505
24533
  currentOrg: org || undefined
24506
24534
  };
24535
+ this.authContextCachedAt = Date.now();
24507
24536
  if (!this.orgId && this.authContext.organizations.length > 0) {
24508
24537
  this.orgId = this.authContext.organizations[0].id;
24509
24538
  }
@@ -24581,6 +24610,7 @@ KanboClient.prototype.removeProjectMember = async function(projectId, userId) {
24581
24610
  };
24582
24611
 
24583
24612
  // src/lib/client/tickets.ts
24613
+ init_errors();
24584
24614
  KanboClient.prototype.listTickets = async function(projectId, params) {
24585
24615
  return this.get(EP.TICKETS.LIST(projectId), params);
24586
24616
  };
@@ -24591,17 +24621,19 @@ KanboClient.prototype.getMyTickets = async function(projectId, params) {
24591
24621
  return this.listTickets(projectId, { ...params, assigneeId });
24592
24622
  }
24593
24623
  const projects = await this.listProjects();
24594
- const allTickets = [];
24595
24624
  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 {}
24625
+ const results = await Promise.allSettled(projects.data.map((project) => this.listTickets(project.id, {
24626
+ ...params,
24627
+ assigneeId,
24628
+ limit: perProjectLimit
24629
+ })));
24630
+ const allTickets = [];
24631
+ for (const result of results) {
24632
+ if (result.status === "fulfilled") {
24633
+ allTickets.push(...result.value.data);
24634
+ } else if (isKanboAPIError(result.reason) && result.reason.statusCode === 401) {
24635
+ throw result.reason;
24636
+ }
24605
24637
  }
24606
24638
  return { data: allTickets };
24607
24639
  };
@@ -24974,9 +25006,12 @@ function getKanboClient() {
24974
25006
  if (requestClient) {
24975
25007
  return requestClient;
24976
25008
  }
25009
+ if (CONFIG.TRANSPORT === "http") {
25010
+ throw new APINotConfiguredError("No request-scoped client available. In HTTP mode, all tool calls must run within runWithClient().");
25011
+ }
24977
25012
  if (!clientInstance) {
24978
25013
  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.");
25014
+ throw new APINotConfiguredError('No API key available. Set KANBO_API_KEY environment variable or run "kanbo-mcp login".');
24980
25015
  }
24981
25016
  clientInstance = new KanboClient({
24982
25017
  apiKey: CONFIG.KANBO_API_KEY,
@@ -24986,7 +25021,11 @@ function getKanboClient() {
24986
25021
  }
24987
25022
  return clientInstance;
24988
25023
  }
25024
+ // src/server.ts
25025
+ init_errors();
25026
+
24989
25027
  // src/tools/types.ts
25028
+ init_errors();
24990
25029
  function successResult(data) {
24991
25030
  return { success: true, data };
24992
25031
  }
@@ -25007,6 +25046,47 @@ function errorResultFromError(error2) {
25007
25046
  const details = isAppError(error2) ? error2.details : undefined;
25008
25047
  return errorResult(code, message, details);
25009
25048
  }
25049
+ var JSON_TYPE_CHECKS = {
25050
+ string: (v) => typeof v === "string",
25051
+ number: (v) => typeof v === "number",
25052
+ integer: (v) => typeof v === "number" && Number.isInteger(v),
25053
+ boolean: (v) => typeof v === "boolean",
25054
+ array: (v) => Array.isArray(v),
25055
+ object: (v) => typeof v === "object" && v !== null && !Array.isArray(v)
25056
+ };
25057
+ function validateToolArgs(args, schema) {
25058
+ if (!schema || schema.type !== "object")
25059
+ return null;
25060
+ const properties = schema.properties ?? {};
25061
+ const required2 = schema.required ?? [];
25062
+ for (const key of required2) {
25063
+ if (args[key] === undefined || args[key] === null) {
25064
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, `Missing required argument: ${key}`);
25065
+ }
25066
+ }
25067
+ for (const [key, value] of Object.entries(args)) {
25068
+ if (value === undefined || value === null)
25069
+ continue;
25070
+ const prop = properties[key];
25071
+ if (!prop?.type)
25072
+ continue;
25073
+ const checker = JSON_TYPE_CHECKS[prop.type];
25074
+ if (checker && !checker(value)) {
25075
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, `Argument '${key}' must be of type ${prop.type}, got ${typeof value}`);
25076
+ }
25077
+ }
25078
+ return null;
25079
+ }
25080
+ function withArgValidation(tool, handler) {
25081
+ if (!tool.inputSchema)
25082
+ return handler;
25083
+ return async (args) => {
25084
+ const validationError = validateToolArgs(args, tool.inputSchema);
25085
+ if (validationError)
25086
+ return validationError;
25087
+ return handler(args);
25088
+ };
25089
+ }
25010
25090
  function formatToolResult(result) {
25011
25091
  return JSON.stringify(result, null, 2);
25012
25092
  }
@@ -25339,6 +25419,8 @@ var projectTools = [
25339
25419
  ];
25340
25420
 
25341
25421
  // src/tools/tickets.ts
25422
+ init_errors();
25423
+ init_constants();
25342
25424
  var listTicketsTool = {
25343
25425
  tool: {
25344
25426
  name: "list_tickets",
@@ -25457,6 +25539,12 @@ var getTicketByKeyTool = {
25457
25539
  const client = getKanboClient();
25458
25540
  const authContext = client.getAuthContext();
25459
25541
  let orgSlug = args.orgSlug;
25542
+ if (orgSlug && authContext?.organizations?.length) {
25543
+ const isAuthorized = authContext.organizations.some((o) => o.slug === orgSlug);
25544
+ if (!isAuthorized) {
25545
+ return errorResult(ERROR_CODES.NO_ORG_SELECTED, "You do not have access to the specified organization.");
25546
+ }
25547
+ }
25460
25548
  if (!orgSlug && authContext?.currentOrg) {
25461
25549
  orgSlug = authContext.currentOrg.slug;
25462
25550
  }
@@ -26658,6 +26746,7 @@ var workflowTools = [
26658
26746
  ];
26659
26747
 
26660
26748
  // src/tools/comments.ts
26749
+ init_constants();
26661
26750
  var listCommentsTool = {
26662
26751
  tool: {
26663
26752
  name: "list_comments",
@@ -26837,6 +26926,7 @@ var commentTools = [
26837
26926
  ];
26838
26927
 
26839
26928
  // src/tools/tags.ts
26929
+ init_constants();
26840
26930
  var listTagsTool = {
26841
26931
  tool: {
26842
26932
  name: "list_tags",
@@ -28583,7 +28673,7 @@ The AI gathers project context automatically (related tickets, statuses, workflo
28583
28673
  var findSimilarTicketsTool = {
28584
28674
  tool: {
28585
28675
  name: "find_similar_tickets",
28586
- description: "Semantic search for similar tickets. Use BEFORE creating a ticket to check for duplicates. Also useful for finding related work. Returns similarity scores.",
28676
+ description: "Semantic search for similar tickets using vector similarity (cosine). Use cases: (1) BEFORE creating a ticket to check for duplicates, (2) find related work to a given ticket — pair with get_completed_tickets to discover closed tickets that addressed the same topic, (3) general discovery of thematically related tickets across the project. Returns results ranked by similarity score.",
28587
28677
  inputSchema: {
28588
28678
  type: "object",
28589
28679
  properties: {
@@ -28823,6 +28913,7 @@ var usageTools = [
28823
28913
  ];
28824
28914
 
28825
28915
  // src/tools/organization.ts
28916
+ init_errors();
28826
28917
  var listOrganizationsTool = {
28827
28918
  tool: {
28828
28919
  name: "list_organizations",
@@ -29051,6 +29142,8 @@ var watcherTools = [
29051
29142
  ];
29052
29143
 
29053
29144
  // src/tools/batch.ts
29145
+ init_errors();
29146
+ init_constants();
29054
29147
  var BATCH_CONCURRENCY_LIMIT = 5;
29055
29148
  async function mapWithConcurrency(items, fn, concurrency = BATCH_CONCURRENCY_LIMIT) {
29056
29149
  const results = new Array(items.length);
@@ -29448,7 +29541,7 @@ var TOOL_INDEX = {
29448
29541
  },
29449
29542
  tags: "Apply 2-5 tags per ticket AFTER creation using set_ticket_tags. Use lowercase-kebab-case. Common categories: feature area (auth, billing), technology (react, api), work type (feature, bug, refactor). Create missing tags with create_tag first.",
29450
29543
  ticket_type: "Always set typeSlug when creating tickets. Common types: feature, bug, task, improvement, story, spike, docs. Use list_ticket_types to see project-specific types. The typeSlug determines which template_structure sections to use.",
29451
- related_tickets: "ALWAYS call find_similar_tickets before creating a ticket to check for duplicates and related work. If a related parent ticket exists, link via parentId when creating. Use get_ticket_children to view subtasks of a ticket.",
29544
+ related_tickets: "ALWAYS call find_similar_tickets before creating a ticket to check for duplicates and related work. If a related parent ticket exists, link via parentId when creating. Use get_ticket_children to view subtasks of a ticket. To find closed tickets related to a specific ticket, call find_similar_tickets with the ticket title/description, then cross-reference results with get_completed_tickets.",
29452
29545
  ai_workflow: "For best results, use the AI tools in this order: find_similar_tickets (check duplicates/related) → check_clarity (verify title) → generate_description (AI-generate HTML + acceptance criteria) → suggest_attributes (AI-suggest priority, size, tags) → create_ticket (ALWAYS include acceptanceCriteria + parentId if related) → set_ticket_tags. The generate_description tool gathers project context (related tickets, workflow) automatically."
29453
29546
  },
29454
29547
  categories: {
@@ -29527,8 +29620,33 @@ var TOOL_INDEX = {
29527
29620
  "Usage & Billing": {
29528
29621
  tools: ["get_mcp_usage", "get_ai_usage"],
29529
29622
  description: "Check MCP request and AI token quotas."
29623
+ },
29624
+ Git: {
29625
+ tools: ["get_commit_prefix"],
29626
+ description: "Git commit conventions. Use get_commit_prefix to generate properly formatted commit message prefixes from ticket keys."
29530
29627
  }
29531
29628
  },
29629
+ git_conventions: {
29630
+ 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.",
29631
+ commit_message_format: {
29632
+ single_ticket: "<TICKET_KEY>: <description>",
29633
+ multiple_tickets: "<TICKET_KEY_1> <TICKET_KEY_2>: <description>",
29634
+ examples: [
29635
+ "WHLZ-59: implement Google OAuth login flow",
29636
+ "WHLZ-59 WHLZ-60: add shared auth middleware for OAuth providers",
29637
+ "UNO-12: fix pagination bug in ticket list"
29638
+ ]
29639
+ },
29640
+ rules: [
29641
+ "ALWAYS prefix commit messages with the ticket key(s) you are working on",
29642
+ "The ticket key format is PROJECT_PREFIX-NUMBER (e.g., WHLZ-59, UNO-12)",
29643
+ "Use get_commit_prefix tool to generate the correct prefix for one or more tickets",
29644
+ "The prefix links commits to tickets in the Kanbo UI Git tab — without it, commits are invisible to the ticket",
29645
+ 'Use lowercase description after the prefix, starting with a verb (e.g., "add", "fix", "update", "refactor")',
29646
+ "Keep the first line under 72 characters"
29647
+ ],
29648
+ 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"
29649
+ },
29532
29650
  common_workflows: {
29533
29651
  "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
29652
  "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 +29655,8 @@ var TOOL_INDEX = {
29537
29655
  "Daily standup": "get_my_tickets + get_overdue_tickets + get_tickets_due_soon",
29538
29656
  "Release shipping": "get_release_tickets (verify all complete) → mark_release_shipped",
29539
29657
  "Bulk cleanup": "list_tickets (filter) → batch_complete_tickets or batch_move_tickets",
29540
- "Team workload review": "get_member_analytics + get_distribution_analytics"
29658
+ "Team workload review": "get_member_analytics + get_distribution_analytics",
29659
+ "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
29660
  }
29542
29661
  };
29543
29662
  var helpTools = [
@@ -29576,6 +29695,54 @@ var helpTools = [
29576
29695
  }
29577
29696
  }
29578
29697
  ];
29698
+
29699
+ // src/tools/git.ts
29700
+ init_errors();
29701
+ var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
29702
+ var getCommitPrefixTool = {
29703
+ tool: {
29704
+ name: "get_commit_prefix",
29705
+ 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.",
29706
+ inputSchema: {
29707
+ type: "object",
29708
+ properties: {
29709
+ ticketKeys: {
29710
+ type: "array",
29711
+ items: { type: "string" },
29712
+ description: 'One or more ticket keys (e.g., ["WHLZ-59"] or ["WHLZ-59", "WHLZ-60"]). ' + "Use the ticket key format: PROJECT_PREFIX-NUMBER."
29713
+ }
29714
+ },
29715
+ required: ["ticketKeys"]
29716
+ }
29717
+ },
29718
+ handler: async (args) => {
29719
+ const ticketKeys = args.ticketKeys;
29720
+ if (!Array.isArray(ticketKeys) || ticketKeys.length === 0) {
29721
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, 'ticketKeys must be a non-empty array of ticket keys (e.g., ["WHLZ-59"])');
29722
+ }
29723
+ const invalidKeys = ticketKeys.filter((key) => !TICKET_KEY_PATTERN.test(key));
29724
+ if (invalidKeys.length > 0) {
29725
+ return errorResult(ERROR_CODES.INVALID_ARGUMENTS, `Invalid ticket key format: ${invalidKeys.join(", ")}. Expected format: PROJECT_PREFIX-NUMBER (e.g., WHLZ-59, UNO-12)`);
29726
+ }
29727
+ const uniqueKeys = [...new Set(ticketKeys)];
29728
+ const prefix = uniqueKeys.join(" ");
29729
+ return successResult({
29730
+ prefix: `${prefix}:`,
29731
+ example: `${prefix}: <describe your change>`,
29732
+ format_rules: [
29733
+ "Use lowercase description after the prefix",
29734
+ "Start with a verb: add, fix, update, refactor, remove, implement",
29735
+ "Keep the first line under 72 characters"
29736
+ ],
29737
+ examples: [
29738
+ `${prefix}: implement the feature`,
29739
+ `${prefix}: fix validation bug`,
29740
+ `${prefix}: update error handling`
29741
+ ]
29742
+ });
29743
+ }
29744
+ };
29745
+ var gitTools = [getCommitPrefixTool];
29579
29746
  // src/tools/index.ts
29580
29747
  var allToolDefinitions = [
29581
29748
  ...helpTools,
@@ -29597,7 +29764,8 @@ var allToolDefinitions = [
29597
29764
  ...analyticsTools,
29598
29765
  ...notificationTools,
29599
29766
  ...aiTools,
29600
- ...usageTools
29767
+ ...usageTools,
29768
+ ...gitTools
29601
29769
  ];
29602
29770
 
29603
29771
  class ToolRegistryImpl {
@@ -29610,7 +29778,7 @@ class ToolRegistryImpl {
29610
29778
  }
29611
29779
  register(definition) {
29612
29780
  this.tools.push(definition.tool);
29613
- this.handlers.set(definition.tool.name, definition.handler);
29781
+ this.handlers.set(definition.tool.name, withArgValidation(definition.tool, definition.handler));
29614
29782
  }
29615
29783
  getTool(name) {
29616
29784
  return this.tools.find((t) => t.name === name);
@@ -43942,6 +44110,9 @@ var _Elysia = class _Elysia2 {
43942
44110
  };
43943
44111
  var Elysia = _Elysia;
43944
44112
 
44113
+ // src/server-http.ts
44114
+ init_constants();
44115
+
43945
44116
  // src/config/routes.ts
43946
44117
  var ROUTES = {
43947
44118
  HEALTH: {
@@ -43959,6 +44130,7 @@ var ROUTES = {
43959
44130
  };
43960
44131
 
43961
44132
  // src/server-http.ts
44133
+ init_errors();
43962
44134
  var httpLogger = logger.withCategory(LogCategory.SYSTEM);
43963
44135
  function extractApiKey(request) {
43964
44136
  const apiKeyHeader = request.headers.get("x-kanbo-api-key");
@@ -43977,6 +44149,12 @@ function extractApiKey(request) {
43977
44149
  }
43978
44150
  return null;
43979
44151
  }
44152
+ function jsonErrorResponse(status2, code, message) {
44153
+ return new Response(JSON.stringify({
44154
+ success: false,
44155
+ error: { code, message, timestamp: new Date().toISOString() }
44156
+ }), { status: status2, headers: { "Content-Type": "application/json" } });
44157
+ }
43980
44158
  function unauthorizedResponse(message) {
43981
44159
  return new Response(JSON.stringify({
43982
44160
  success: false,
@@ -43992,7 +44170,36 @@ function unauthorizedResponse(message) {
43992
44170
  }
43993
44171
  async function startHttpServer() {
43994
44172
  httpLogger.info("Starting in HTTP mode with per-request authentication");
43995
- const app = new Elysia().get(ROUTES.HEALTH.INDEX, () => ({
44173
+ const app = new Elysia().onRequest(({ request }) => {
44174
+ const origin = request.headers.get("origin");
44175
+ if (request.method === "OPTIONS") {
44176
+ const headers = {
44177
+ "Access-Control-Max-Age": "86400",
44178
+ "Access-Control-Allow-Methods": "GET, POST",
44179
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Kanbo-API-Key"
44180
+ };
44181
+ if (origin && CONFIG.CORS_ORIGINS.includes(origin)) {
44182
+ headers["Access-Control-Allow-Origin"] = origin;
44183
+ headers["Vary"] = "Origin";
44184
+ }
44185
+ return new Response(null, { status: 204, headers });
44186
+ }
44187
+ }).onAfterHandle(({ request, response }) => {
44188
+ const origin = request.headers.get("origin");
44189
+ if (origin && CONFIG.CORS_ORIGINS.includes(origin) && response instanceof Response) {
44190
+ response.headers.set("Access-Control-Allow-Origin", origin);
44191
+ response.headers.set("Vary", "Origin");
44192
+ }
44193
+ }).onParse(({ request }) => {
44194
+ const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
44195
+ if (contentLength > CONFIG.MAX_BODY_SIZE) {
44196
+ throw new Error(`PAYLOAD_TOO_LARGE`);
44197
+ }
44198
+ }).onError(({ error: error2 }) => {
44199
+ if (error2.message === "PAYLOAD_TOO_LARGE") {
44200
+ return jsonErrorResponse(413, ERROR_CODES.VALIDATION_ERROR, `Request body too large. Maximum size is ${CONFIG.MAX_BODY_SIZE} bytes.`);
44201
+ }
44202
+ }).get(ROUTES.HEALTH.INDEX, () => ({
43996
44203
  status: "ok",
43997
44204
  server: SERVER_INFO.name,
43998
44205
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanbodev/mcp",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "MCP (Model Context Protocol) server for Kanbo - AI-native project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",