@kanbodev/mcp 1.1.5 → 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 +479 -272
- package/docs/GIT_CONVENTIONS.md +139 -0
- package/package.json +1 -1
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 <
|
|
15863
|
+
if (!apiKey || apiKey.length < 10) {
|
|
15621
15864
|
return "***";
|
|
15622
15865
|
}
|
|
15623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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/
|
|
16647
|
-
|
|
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
|
-
];
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
24597
|
-
|
|
24598
|
-
|
|
24599
|
-
|
|
24600
|
-
|
|
24601
|
-
|
|
24602
|
-
|
|
24603
|
-
|
|
24604
|
-
|
|
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(
|
|
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",
|
|
@@ -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);
|
|
@@ -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().
|
|
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
|