@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.
@@ -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
@@ -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
- const res = await fetch(url, {
34
- method,
35
- headers,
36
- body: body !== undefined ? JSON.stringify(body) : undefined,
37
- });
38
- // Auto-retry na 409 Conflict (optimistic locking) — max 3 pokusy
39
- if (res.status === 409 && retryCount < 3) {
40
- await new Promise((r) => setTimeout(r, 200 * (retryCount + 1)));
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
- return { apiUrl, apiKey, defaultProject };
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
- return {
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
- return {
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: "0.1.9",
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,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare function registerAddEpicDependency(server: McpServer, api: ApiClient): void;
4
+ //# sourceMappingURL=add-epic-dependency.d.ts.map
@@ -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
@@ -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.completedTickets}/${e.totalTickets})${e.targetDate ? ` target: ${e.targetDate}` : ""}`)
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
- totalTickets: number;
97
- completedTickets: number;
96
+ ticketCount: number;
97
+ completedTicketCount: number;
98
98
  createdAt: string;
99
99
  }
100
100
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-system/tickr-mcp",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "MCP server for Tickr project management — 56 tools + setup CLI wizard",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",