@k-system/tickr-mcp 1.20.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
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../api-client.js";
3
+ /**
4
+ * Dynamic resource tickr://pending-tasks
5
+ * Vrací aktuální stav dev queue — počet čekajících tasků a detail dalšího v řadě.
6
+ * Aktualizuje se přes sendResourceListChanged() při DevTaskQueued eventu.
7
+ */
8
+ export declare function registerPendingTasksResource(server: McpServer, api: ApiClient): void;
9
+ //# sourceMappingURL=pending-tasks-resource.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Dynamic resource tickr://pending-tasks
3
+ * Vrací aktuální stav dev queue — počet čekajících tasků a detail dalšího v řadě.
4
+ * Aktualizuje se přes sendResourceListChanged() při DevTaskQueued eventu.
5
+ */
6
+ export function registerPendingTasksResource(server, api) {
7
+ server.resource("pending-tasks", "tickr://pending-tasks", { description: "Current dev queue status — queued task count and next task preview" }, async (uri) => {
8
+ // Heartbeat vrací počet queued tasků bez side-effectu na assignment
9
+ const heartbeat = await api
10
+ .post("/api/dev-queue/heartbeat", {})
11
+ .catch(() => null);
12
+ const queuedTasks = heartbeat?.queuedTasks ?? 0;
13
+ const agentStatus = heartbeat?.agentStatus ?? "unknown";
14
+ let text = `## Dev Queue Status\n`;
15
+ text += `- **Agent Status:** ${agentStatus}\n`;
16
+ text += `- **Queued Tasks:** ${queuedTasks}\n`;
17
+ if (queuedTasks > 0) {
18
+ text += `\n⚠️ There are ${queuedTasks} task(s) waiting. Run \`poll_dev_queue\` to pick up the next one.\n`;
19
+ }
20
+ else {
21
+ text += `\n✅ Queue is empty — no pending tasks.\n`;
22
+ }
23
+ return {
24
+ contents: [
25
+ {
26
+ uri: uri.href,
27
+ mimeType: "text/markdown",
28
+ text,
29
+ },
30
+ ],
31
+ };
32
+ });
33
+ }
34
+ //# sourceMappingURL=pending-tasks-resource.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";
@@ -90,14 +93,23 @@ import { registerLinkCommit } from "./tools/link-commit.js";
90
93
  // Resources
91
94
  import { registerTicketResource } from "./resources/ticket-resource.js";
92
95
  import { registerProjectResource } from "./resources/project-resource.js";
96
+ import { registerPendingTasksResource } from "./resources/pending-tasks-resource.js";
93
97
  // SignalR push notifications
94
98
  import { initSignalRClient, stopSignalRClient } from "./signalr-client.js";
95
99
  export async function startServer() {
96
100
  const config = loadConfig();
97
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
+ }
98
110
  const server = new McpServer({
99
111
  name: "tickr",
100
- version: "0.1.8",
112
+ version: "1.22.0",
101
113
  });
102
114
  // Debug logging wrapper (dedup odstraněn — nefunkční cross-process, řeší se na API straně: TKR-ADR-0043)
103
115
  {
@@ -194,6 +206,9 @@ export async function startServer() {
194
206
  registerDeleteCycle(server, api);
195
207
  registerUpdateEpic(server, api);
196
208
  registerDeleteEpic(server, api);
209
+ // ADR-0050: Epic Dependencies
210
+ registerAddEpicDependency(server, api);
211
+ registerRemoveEpicDependency(server, api);
197
212
  // ADR-0026 Phase 2f: Project Members
198
213
  registerListProjectMembers(server, api);
199
214
  registerAddProjectMember(server, api);
@@ -212,14 +227,26 @@ export async function startServer() {
212
227
  // Registrace resources
213
228
  registerTicketResource(server, api);
214
229
  registerProjectResource(server, api);
230
+ registerPendingTasksResource(server, api);
215
231
  // Spuštění na stdio transportu
216
232
  const transport = new StdioServerTransport();
217
233
  await server.connect(transport);
218
234
  // SignalR push notifications — pripojeni na BoardHub pro real-time eventy
219
235
  // Neni kriticke — MCP server funguje i bez SignalR
220
236
  initSignalRClient(config, server, config.defaultProject).catch(() => { });
237
+ // Initial heartbeat — nastav agenta jako idle
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
+ });
241
+ // Periodicky heartbeat kazdych 60 sekund — udržuje agent status + snižuje latenci task discovery
242
+ const heartbeatInterval = setInterval(() => {
243
+ api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
244
+ console.error("[tickr-mcp] heartbeat failed:", err instanceof Error ? err.message : String(err));
245
+ });
246
+ }, 60_000);
221
247
  // Cleanup pri ukonceni procesu
222
248
  const cleanup = async () => {
249
+ clearInterval(heartbeatInterval);
223
250
  await stopSignalRClient();
224
251
  process.exit(0);
225
252
  };
@@ -19,8 +19,13 @@ export async function initSignalRClient(config, server, projectSlug) {
19
19
  .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
20
20
  .configureLogging(LogLevel.None)
21
21
  .build();
22
- // Event handlery — notifikace do MCP session
22
+ // Event handlery — resource update + logging notifikace do MCP session
23
23
  connection.on("DevTaskQueued", () => {
24
+ // Aktualizuj tickr://pending-tasks resource — agent uvidí změnu při dalším tool callu
25
+ try {
26
+ server.sendResourceListChanged();
27
+ }
28
+ catch { }
24
29
  server
25
30
  .sendLoggingMessage({
26
31
  level: "notice",
@@ -30,6 +35,10 @@ export async function initSignalRClient(config, server, projectSlug) {
30
35
  .catch(() => { });
31
36
  });
32
37
  connection.on("DevTaskCompleted", () => {
38
+ try {
39
+ server.sendResourceListChanged();
40
+ }
41
+ catch { }
33
42
  server
34
43
  .sendLoggingMessage({
35
44
  level: "info",
@@ -39,6 +48,10 @@ export async function initSignalRClient(config, server, projectSlug) {
39
48
  .catch(() => { });
40
49
  });
41
50
  connection.on("TicketUpdated", () => {
51
+ try {
52
+ server.sendResourceListChanged();
53
+ }
54
+ catch { }
42
55
  server
43
56
  .sendLoggingMessage({
44
57
  level: "debug",
@@ -48,6 +61,10 @@ export async function initSignalRClient(config, server, projectSlug) {
48
61
  .catch(() => { });
49
62
  });
50
63
  connection.on("TicketCreated", () => {
64
+ try {
65
+ server.sendResourceListChanged();
66
+ }
67
+ catch { }
51
68
  server
52
69
  .sendLoggingMessage({
53
70
  level: "debug",
@@ -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.20.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",