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