@k-system/tickr-mcp 1.21.0 → 1.22.0
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/api-client.d.ts +4 -0
- package/dist/api-client.js +63 -8
- package/dist/config.js +21 -3
- package/dist/server.js +21 -3
- package/dist/tools/add-epic-dependency.d.ts +4 -0
- package/dist/tools/add-epic-dependency.js +23 -0
- package/dist/tools/list-epics.js +1 -1
- package/dist/tools/remove-epic-dependency.d.ts +4 -0
- package/dist/tools/remove-epic-dependency.js +22 -0
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export declare class ApiClient {
|
|
|
24
24
|
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
25
25
|
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
26
26
|
delete<T = unknown>(path: string): Promise<T>;
|
|
27
|
+
/** Health check — vrací true pokud API odpovídá */
|
|
28
|
+
healthCheck(): Promise<boolean>;
|
|
27
29
|
private request;
|
|
30
|
+
private shouldRetry;
|
|
31
|
+
private getRetryDelay;
|
|
28
32
|
}
|
|
29
33
|
//# sourceMappingURL=api-client.d.ts.map
|
package/dist/api-client.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getAuthHeader } from "./auth.js";
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
const RETRY_BASE_MS = 200;
|
|
2
5
|
/** Fetch wrapper pro Tickr REST API s automatickou autentizací */
|
|
3
6
|
export class ApiClient {
|
|
4
7
|
config;
|
|
@@ -20,6 +23,20 @@ export class ApiClient {
|
|
|
20
23
|
async delete(path) {
|
|
21
24
|
return this.request("DELETE", path);
|
|
22
25
|
}
|
|
26
|
+
/** Health check — vrací true pokud API odpovídá */
|
|
27
|
+
async healthCheck() {
|
|
28
|
+
try {
|
|
29
|
+
const url = `${this.config.apiUrl}/health`;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
32
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
return res.ok;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
async request(method, path, body, retryCount = 0) {
|
|
24
41
|
const auth = await getAuthHeader(this.config);
|
|
25
42
|
const url = `${this.config.apiUrl}${path}`;
|
|
@@ -30,14 +47,38 @@ export class ApiClient {
|
|
|
30
47
|
if (body !== undefined) {
|
|
31
48
|
headers["Content-Type"] = "application/json";
|
|
32
49
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
// Fetch s timeout přes AbortController
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
headers,
|
|
58
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
65
|
+
throw new Error(`Request timeout after ${REQUEST_TIMEOUT_MS / 1000}s: ${method} ${path}`);
|
|
66
|
+
}
|
|
67
|
+
// Síťová chyba — retry pokud máme pokusy
|
|
68
|
+
if (retryCount < MAX_RETRIES) {
|
|
69
|
+
const delay = RETRY_BASE_MS * Math.pow(2, retryCount);
|
|
70
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
71
|
+
return this.request(method, path, body, retryCount + 1);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
}
|
|
78
|
+
// Auto-retry na 409 Conflict, 429 Too Many Requests, 5xx — max 3 pokusy
|
|
79
|
+
if (this.shouldRetry(res.status) && retryCount < MAX_RETRIES) {
|
|
80
|
+
const delay = this.getRetryDelay(res, retryCount);
|
|
81
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
41
82
|
return this.request(method, path, body, retryCount + 1);
|
|
42
83
|
}
|
|
43
84
|
if (!res.ok) {
|
|
@@ -66,5 +107,19 @@ export class ApiClient {
|
|
|
66
107
|
}
|
|
67
108
|
return json.data;
|
|
68
109
|
}
|
|
110
|
+
shouldRetry(status) {
|
|
111
|
+
return status === 409 || status === 429 || status >= 500;
|
|
112
|
+
}
|
|
113
|
+
getRetryDelay(res, retryCount) {
|
|
114
|
+
// Respektovat Retry-After header pokud existuje (429)
|
|
115
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
116
|
+
if (retryAfter) {
|
|
117
|
+
const seconds = parseInt(retryAfter, 10);
|
|
118
|
+
if (!isNaN(seconds))
|
|
119
|
+
return seconds * 1000;
|
|
120
|
+
}
|
|
121
|
+
// Exponential backoff: 200ms, 400ms, 800ms
|
|
122
|
+
return RETRY_BASE_MS * Math.pow(2, retryCount);
|
|
123
|
+
}
|
|
69
124
|
}
|
|
70
125
|
//# sourceMappingURL=api-client.js.map
|
package/dist/config.js
CHANGED
|
@@ -8,26 +8,44 @@ export function loadConfig() {
|
|
|
8
8
|
const defaultProject = process.env.TICKR_DEFAULT_PROJECT;
|
|
9
9
|
// Env vars mají prioritu
|
|
10
10
|
if (apiUrl && apiKey) {
|
|
11
|
-
|
|
11
|
+
const config = { apiUrl, apiKey, defaultProject };
|
|
12
|
+
validateConfig(config);
|
|
13
|
+
return config;
|
|
12
14
|
}
|
|
13
15
|
// Fallback na config soubor
|
|
14
16
|
const configPath = join(homedir(), ".tickr", "config.json");
|
|
15
17
|
try {
|
|
16
18
|
const raw = readFileSync(configPath, "utf-8");
|
|
17
19
|
const file = JSON.parse(raw);
|
|
18
|
-
|
|
20
|
+
const config = {
|
|
19
21
|
apiUrl: apiUrl || file.apiUrl || "https://localhost:6001",
|
|
20
22
|
apiKey: apiKey || file.apiKey || "",
|
|
21
23
|
defaultProject: defaultProject || file.defaultProject,
|
|
22
24
|
};
|
|
25
|
+
validateConfig(config);
|
|
26
|
+
return config;
|
|
23
27
|
}
|
|
24
28
|
catch {
|
|
25
29
|
// Config soubor neexistuje — použijeme defaults
|
|
26
|
-
|
|
30
|
+
const config = {
|
|
27
31
|
apiUrl: apiUrl || "https://localhost:6001",
|
|
28
32
|
apiKey: apiKey || "",
|
|
29
33
|
defaultProject,
|
|
30
34
|
};
|
|
35
|
+
validateConfig(config);
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Validace konfigurace — loguje varování pro problematické hodnoty */
|
|
40
|
+
function validateConfig(config) {
|
|
41
|
+
if (!config.apiKey) {
|
|
42
|
+
console.error("[tickr-mcp] WARNING: No API key configured — tools will fail with 401");
|
|
43
|
+
}
|
|
44
|
+
if (config.apiUrl && !config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("https://")) {
|
|
45
|
+
console.error(`[tickr-mcp] WARNING: apiUrl "${config.apiUrl}" doesn't start with http(s)://`);
|
|
46
|
+
}
|
|
47
|
+
if (config.apiUrl?.endsWith("/")) {
|
|
48
|
+
config.apiUrl = config.apiUrl.replace(/\/+$/, "");
|
|
31
49
|
}
|
|
32
50
|
}
|
|
33
51
|
//# sourceMappingURL=config.js.map
|
package/dist/server.js
CHANGED
|
@@ -72,6 +72,9 @@ import { registerUpdateCycle } from "./tools/update-cycle.js";
|
|
|
72
72
|
import { registerDeleteCycle } from "./tools/delete-cycle.js";
|
|
73
73
|
import { registerUpdateEpic } from "./tools/update-epic.js";
|
|
74
74
|
import { registerDeleteEpic } from "./tools/delete-epic.js";
|
|
75
|
+
// ADR-0050: Epic Dependencies
|
|
76
|
+
import { registerAddEpicDependency } from "./tools/add-epic-dependency.js";
|
|
77
|
+
import { registerRemoveEpicDependency } from "./tools/remove-epic-dependency.js";
|
|
75
78
|
// ADR-0026 Phase 2f: Project Members
|
|
76
79
|
import { registerListProjectMembers } from "./tools/list-project-members.js";
|
|
77
80
|
import { registerAddProjectMember } from "./tools/add-project-member.js";
|
|
@@ -96,9 +99,17 @@ import { initSignalRClient, stopSignalRClient } from "./signalr-client.js";
|
|
|
96
99
|
export async function startServer() {
|
|
97
100
|
const config = loadConfig();
|
|
98
101
|
const api = new ApiClient(config);
|
|
102
|
+
// Startup health check — ověření dostupnosti API
|
|
103
|
+
const healthy = await api.healthCheck();
|
|
104
|
+
if (healthy) {
|
|
105
|
+
console.error(`[tickr-mcp] API reachable at ${config.apiUrl}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(`[tickr-mcp] WARNING: API unreachable at ${config.apiUrl} — tools may fail`);
|
|
109
|
+
}
|
|
99
110
|
const server = new McpServer({
|
|
100
111
|
name: "tickr",
|
|
101
|
-
version: "
|
|
112
|
+
version: "1.22.0",
|
|
102
113
|
});
|
|
103
114
|
// Debug logging wrapper (dedup odstraněn — nefunkční cross-process, řeší se na API straně: TKR-ADR-0043)
|
|
104
115
|
{
|
|
@@ -195,6 +206,9 @@ export async function startServer() {
|
|
|
195
206
|
registerDeleteCycle(server, api);
|
|
196
207
|
registerUpdateEpic(server, api);
|
|
197
208
|
registerDeleteEpic(server, api);
|
|
209
|
+
// ADR-0050: Epic Dependencies
|
|
210
|
+
registerAddEpicDependency(server, api);
|
|
211
|
+
registerRemoveEpicDependency(server, api);
|
|
198
212
|
// ADR-0026 Phase 2f: Project Members
|
|
199
213
|
registerListProjectMembers(server, api);
|
|
200
214
|
registerAddProjectMember(server, api);
|
|
@@ -221,10 +235,14 @@ export async function startServer() {
|
|
|
221
235
|
// Neni kriticke — MCP server funguje i bez SignalR
|
|
222
236
|
initSignalRClient(config, server, config.defaultProject).catch(() => { });
|
|
223
237
|
// Initial heartbeat — nastav agenta jako idle
|
|
224
|
-
api.post("/api/dev-queue/heartbeat", {}).catch(() => {
|
|
238
|
+
api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
|
|
239
|
+
console.error("[tickr-mcp] initial heartbeat failed:", err instanceof Error ? err.message : String(err));
|
|
240
|
+
});
|
|
225
241
|
// Periodicky heartbeat kazdych 60 sekund — udržuje agent status + snižuje latenci task discovery
|
|
226
242
|
const heartbeatInterval = setInterval(() => {
|
|
227
|
-
api.post("/api/dev-queue/heartbeat", {}).catch(() => {
|
|
243
|
+
api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
|
|
244
|
+
console.error("[tickr-mcp] heartbeat failed:", err instanceof Error ? err.message : String(err));
|
|
245
|
+
});
|
|
228
246
|
}, 60_000);
|
|
229
247
|
// Cleanup pri ukonceni procesu
|
|
230
248
|
const cleanup = async () => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerAddEpicDependency(server, api) {
|
|
3
|
+
server.tool("add_epic_dependency", "Add a dependency between two epics (predecessor blocks successor)", {
|
|
4
|
+
project: z.string().describe("Project slug"),
|
|
5
|
+
predecessor_epic_id: z.string().describe("UUID of the predecessor epic (must finish first)"),
|
|
6
|
+
successor_epic_id: z.string().describe("UUID of the successor epic (depends on predecessor)"),
|
|
7
|
+
type: z.string().optional().describe("Dependency type: finish-to-start (default), start-to-start, finish-to-finish, start-to-finish"),
|
|
8
|
+
}, async (params) => {
|
|
9
|
+
try {
|
|
10
|
+
const result = await api.post(`/api/projects/${params.project}/epics/${params.predecessor_epic_id}/dependencies`, { successorEpicId: params.successor_epic_id, dependencyType: params.type });
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: "text", text: `Dependency created (${params.type ?? "finish-to-start"}): ${params.predecessor_epic_id} → ${params.successor_epic_id}` }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=add-epic-dependency.js.map
|
package/dist/tools/list-epics.js
CHANGED
|
@@ -6,7 +6,7 @@ export function registerListEpics(server, api) {
|
|
|
6
6
|
try {
|
|
7
7
|
const epics = await api.get(`/api/projects/${params.project}/epics`);
|
|
8
8
|
const text = epics
|
|
9
|
-
.map((e) => `${e.name} (${e.id}) [${e.status}] (${e.
|
|
9
|
+
.map((e) => `${e.name} (${e.id}) [${e.status}] (${e.completedTicketCount}/${e.ticketCount})${e.targetDate ? ` target: ${e.targetDate}` : ""}`)
|
|
10
10
|
.join("\n");
|
|
11
11
|
return {
|
|
12
12
|
content: [{ type: "text", text: text || "No epics found." }],
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ApiClient } from "../api-client.js";
|
|
3
|
+
export declare function registerRemoveEpicDependency(server: McpServer, api: ApiClient): void;
|
|
4
|
+
//# sourceMappingURL=remove-epic-dependency.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerRemoveEpicDependency(server, api) {
|
|
3
|
+
server.tool("remove_epic_dependency", "Remove a dependency between two epics", {
|
|
4
|
+
project: z.string().describe("Project slug"),
|
|
5
|
+
epic_id: z.string().describe("UUID of either epic in the dependency"),
|
|
6
|
+
dependency_id: z.string().describe("UUID of the dependency to remove"),
|
|
7
|
+
}, async (params) => {
|
|
8
|
+
try {
|
|
9
|
+
await api.delete(`/api/projects/${params.project}/epics/${params.epic_id}/dependencies/${params.dependency_id}`);
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: `Dependency ${params.dependency_id} removed` }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=remove-epic-dependency.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -93,8 +93,8 @@ export interface Epic {
|
|
|
93
93
|
color: string;
|
|
94
94
|
status: string;
|
|
95
95
|
targetDate: string | null;
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
ticketCount: number;
|
|
97
|
+
completedTicketCount: number;
|
|
98
98
|
createdAt: string;
|
|
99
99
|
}
|
|
100
100
|
//# sourceMappingURL=types.d.ts.map
|