@k-system/tickr-mcp 1.21.0 → 1.23.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/auth.d.ts CHANGED
@@ -10,9 +10,16 @@ export declare function login(config: TickrConfig, username: string, password: s
10
10
  export declare function refreshToken(config: TickrConfig): Promise<TokenPair>;
11
11
  /**
12
12
  * Vrátí Authorization header value.
13
- * Pokud config má apiKey (tk_...), použije ho přímo.
14
- * Jinak použije JWT token (auto-refresh pokud expiruje).
13
+ * Priorita:
14
+ * 1. API key (tkr_...) přímé použití, žádná expiry
15
+ * 2. Device Grant session (~/.tickr/sessions/) — auto-refresh
16
+ * 3. JWT token (login/password) — auto-refresh
15
17
  */
16
18
  export declare function getAuthHeader(config: TickrConfig): Promise<string>;
19
+ /**
20
+ * Inicializace device grant session při startu serveru.
21
+ * Načte existující session z disku — pokud existuje a je platná, nastaví cache.
22
+ */
23
+ export declare function initDeviceGrantSession(config: TickrConfig): void;
17
24
  export {};
18
25
  //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js CHANGED
@@ -1,4 +1,7 @@
1
+ import { getDeviceGrantToken, loadSession, refreshAccessToken, saveSession, } from "./device-grant.js";
1
2
  let currentTokens = null;
3
+ // Aktuální device grant session (pokud je aktivní)
4
+ let activeSession = null;
2
5
  /** Přihlášení přes username/password — vrátí JWT token pair */
3
6
  export async function login(config, username, password) {
4
7
  const res = await fetch(`${config.apiUrl}/api/auth/login`, {
@@ -40,21 +43,74 @@ export async function refreshToken(config) {
40
43
  }
41
44
  /**
42
45
  * Vrátí Authorization header value.
43
- * Pokud config má apiKey (tk_...), použije ho přímo.
44
- * Jinak použije JWT token (auto-refresh pokud expiruje).
46
+ * Priorita:
47
+ * 1. API key (tkr_...) přímé použití, žádná expiry
48
+ * 2. Device Grant session (~/.tickr/sessions/) — auto-refresh
49
+ * 3. JWT token (login/password) — auto-refresh
45
50
  */
46
51
  export async function getAuthHeader(config) {
47
- // API key auth — jednoduché, bez expiry
52
+ // 1. API key auth — jednoduché, bez expiry
48
53
  if (config.apiKey) {
49
54
  return `Bearer ${config.apiKey}`;
50
55
  }
51
- // JWT auth — refresh pokud token brzy vyprší (30s buffer)
56
+ // 2. Device Grant session auto-refresh
57
+ if (config.tenantId && config.agentName) {
58
+ return `Bearer ${await getDeviceGrantAuthToken(config)}`;
59
+ }
60
+ // 3. JWT auth — refresh pokud token brzy vyprší (30s buffer)
52
61
  if (currentTokens && currentTokens.expiresAt - Date.now() < 30_000) {
53
62
  await refreshToken(config);
54
63
  }
55
64
  if (!currentTokens) {
56
- throw new Error("Not authenticated — set TICKR_API_KEY or call login()");
65
+ throw new Error("Not authenticated — set TICKR_API_KEY or configure device grant");
57
66
  }
58
67
  return `Bearer ${currentTokens.accessToken}`;
59
68
  }
69
+ /**
70
+ * Device Grant auth token s auto-refresh a auto-pairing.
71
+ * Pokud session neexistuje, spustí interaktivní pairing flow.
72
+ */
73
+ async function getDeviceGrantAuthToken(config) {
74
+ // Zkus cache
75
+ if (activeSession && activeSession.expiresAt - Date.now() > 30_000) {
76
+ return activeSession.accessToken;
77
+ }
78
+ // Zkus refresh existující session
79
+ if (activeSession && activeSession.refreshToken) {
80
+ try {
81
+ const result = await refreshAccessToken(config, activeSession.refreshToken);
82
+ activeSession = {
83
+ accessToken: result.access_token,
84
+ refreshToken: result.refresh_token,
85
+ expiresAt: Date.now() + result.expires_in * 1000,
86
+ agentIdentity: result.agent_identity,
87
+ apiUrl: config.apiUrl,
88
+ tenantId: config.tenantId,
89
+ };
90
+ saveSession(config.agentName, activeSession);
91
+ return activeSession.accessToken;
92
+ }
93
+ catch {
94
+ activeSession = null;
95
+ }
96
+ }
97
+ // Načti z disku nebo spusť pairing
98
+ const token = await getDeviceGrantToken(config, config.agentName, config.tenantId);
99
+ // Aktualizuj cache
100
+ activeSession = loadSession(config.agentName);
101
+ return token;
102
+ }
103
+ /**
104
+ * Inicializace device grant session při startu serveru.
105
+ * Načte existující session z disku — pokud existuje a je platná, nastaví cache.
106
+ */
107
+ export function initDeviceGrantSession(config) {
108
+ if (!config.agentName || !config.tenantId)
109
+ return;
110
+ const session = loadSession(config.agentName);
111
+ if (session) {
112
+ activeSession = session;
113
+ console.error(`[tickr] ✓ Connected as ${session.agentIdentity}`);
114
+ }
115
+ }
60
116
  //# sourceMappingURL=auth.js.map
package/dist/config.d.ts CHANGED
@@ -2,6 +2,10 @@ export interface TickrConfig {
2
2
  apiUrl: string;
3
3
  apiKey?: string;
4
4
  defaultProject?: string;
5
+ /** Tenant ID pro device grant auth (UUID) */
6
+ tenantId?: string;
7
+ /** Jméno agenta pro device grant session storage */
8
+ agentName?: string;
5
9
  }
6
10
  /** Načte konfiguraci z env vars nebo ~/.tickr/config.json */
7
11
  export declare function loadConfig(): TickrConfig;
package/dist/config.js CHANGED
@@ -6,28 +6,52 @@ export function loadConfig() {
6
6
  const apiUrl = process.env.TICKR_API_URL;
7
7
  const apiKey = process.env.TICKR_API_KEY;
8
8
  const defaultProject = process.env.TICKR_DEFAULT_PROJECT;
9
+ const tenantId = process.env.TICKR_TENANT_ID;
10
+ const agentName = process.env.TICKR_AGENT_NAME;
9
11
  // Env vars mají prioritu
10
12
  if (apiUrl && apiKey) {
11
- return { apiUrl, apiKey, defaultProject };
13
+ const config = { apiUrl, apiKey, defaultProject, tenantId, agentName };
14
+ validateConfig(config);
15
+ return config;
12
16
  }
13
17
  // Fallback na config soubor
14
18
  const configPath = join(homedir(), ".tickr", "config.json");
15
19
  try {
16
20
  const raw = readFileSync(configPath, "utf-8");
17
21
  const file = JSON.parse(raw);
18
- return {
22
+ const config = {
19
23
  apiUrl: apiUrl || file.apiUrl || "https://localhost:6001",
20
24
  apiKey: apiKey || file.apiKey || "",
21
25
  defaultProject: defaultProject || file.defaultProject,
26
+ tenantId: tenantId || file.tenantId,
27
+ agentName: agentName || file.agentName,
22
28
  };
29
+ validateConfig(config);
30
+ return config;
23
31
  }
24
32
  catch {
25
33
  // Config soubor neexistuje — použijeme defaults
26
- return {
34
+ const config = {
27
35
  apiUrl: apiUrl || "https://localhost:6001",
28
36
  apiKey: apiKey || "",
29
37
  defaultProject,
38
+ tenantId,
39
+ agentName,
30
40
  };
41
+ validateConfig(config);
42
+ return config;
43
+ }
44
+ }
45
+ /** Validace konfigurace — loguje varování pro problematické hodnoty */
46
+ function validateConfig(config) {
47
+ if (!config.apiKey && !config.tenantId) {
48
+ console.error("[tickr-mcp] WARNING: No API key or tenant ID configured — set TICKR_API_KEY or TICKR_TENANT_ID + TICKR_AGENT_NAME for device grant");
49
+ }
50
+ if (config.apiUrl && !config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("https://")) {
51
+ console.error(`[tickr-mcp] WARNING: apiUrl "${config.apiUrl}" doesn't start with http(s)://`);
52
+ }
53
+ if (config.apiUrl?.endsWith("/")) {
54
+ config.apiUrl = config.apiUrl.replace(/\/+$/, "");
31
55
  }
32
56
  }
33
57
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,53 @@
1
+ import type { TickrConfig } from "./config.js";
2
+ interface DeviceCodeResponse {
3
+ device_code: string;
4
+ user_code: string;
5
+ verification_uri: string;
6
+ interval: number;
7
+ expires_in: number;
8
+ }
9
+ interface TokenResponse {
10
+ access_token: string;
11
+ token_type: string;
12
+ expires_in: number;
13
+ refresh_token: string;
14
+ agent_identity: string;
15
+ }
16
+ export interface StoredSession {
17
+ accessToken: string;
18
+ refreshToken: string;
19
+ expiresAt: number;
20
+ agentIdentity: string;
21
+ apiUrl: string;
22
+ tenantId: string;
23
+ }
24
+ /** Načte uloženou session — vrátí null pokud neexistuje nebo je poškozená */
25
+ export declare function loadSession(agentName: string): StoredSession | null;
26
+ /** Uloží session na disk */
27
+ export declare function saveSession(agentName: string, session: StoredSession): void;
28
+ /** Smaže uloženou session */
29
+ export declare function clearSession(agentName: string): void;
30
+ /**
31
+ * Zahájí device authorization flow — vrátí device code a user code.
32
+ * Agent zobrazí user code v terminálu, člověk ho zadá v browseru.
33
+ */
34
+ export declare function initiateDeviceGrant(config: TickrConfig, tenantId: string): Promise<DeviceCodeResponse>;
35
+ /**
36
+ * Polluje /oauth/token dokud člověk neschválí párování.
37
+ * Vrátí access + refresh token, nebo vyhodí chybu při expiraci/zamítnutí.
38
+ */
39
+ export declare function pollForDeviceGrant(config: TickrConfig, deviceCode: string, interval: number, expiresIn: number, onStatus?: (message: string) => void): Promise<TokenResponse>;
40
+ /**
41
+ * Obnoví access token přes refresh token grant.
42
+ * Vrátí nový token pár (rotace — starý refresh token je zneplatněn).
43
+ */
44
+ export declare function refreshAccessToken(config: TickrConfig, refreshToken: string): Promise<TokenResponse>;
45
+ /**
46
+ * Hlavní entry point: získej platný access token.
47
+ * 1. Pokud existuje uložená session a token je platný → vrať
48
+ * 2. Pokud token expiruje → auto-refresh
49
+ * 3. Pokud refresh selže → spusť device grant flow
50
+ */
51
+ export declare function getDeviceGrantToken(config: TickrConfig, agentName: string, tenantId: string, onPairingRequired?: (userCode: string, verificationUri: string) => void): Promise<string>;
52
+ export {};
53
+ //# sourceMappingURL=device-grant.d.ts.map
@@ -0,0 +1,194 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ // --- Session storage ---
5
+ const SESSIONS_DIR = join(homedir(), ".tickr", "sessions");
6
+ function ensureSessionsDir() {
7
+ if (!existsSync(SESSIONS_DIR)) {
8
+ mkdirSync(SESSIONS_DIR, { recursive: true });
9
+ }
10
+ }
11
+ function sessionPath(agentName) {
12
+ const safe = agentName.replace(/[^a-zA-Z0-9_-]/g, "_");
13
+ return join(SESSIONS_DIR, `${safe}.json`);
14
+ }
15
+ /** Načte uloženou session — vrátí null pokud neexistuje nebo je poškozená */
16
+ export function loadSession(agentName) {
17
+ try {
18
+ const path = sessionPath(agentName);
19
+ if (!existsSync(path))
20
+ return null;
21
+ const raw = readFileSync(path, "utf-8");
22
+ return JSON.parse(raw);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** Uloží session na disk */
29
+ export function saveSession(agentName, session) {
30
+ ensureSessionsDir();
31
+ const path = sessionPath(agentName);
32
+ writeFileSync(path, JSON.stringify(session, null, 2), { mode: 0o600 });
33
+ }
34
+ /** Smaže uloženou session */
35
+ export function clearSession(agentName) {
36
+ try {
37
+ const path = sessionPath(agentName);
38
+ if (existsSync(path)) {
39
+ const { unlinkSync } = require("node:fs");
40
+ unlinkSync(path);
41
+ }
42
+ }
43
+ catch {
44
+ // Ignoruj
45
+ }
46
+ }
47
+ // --- Device Authorization Grant flow ---
48
+ /**
49
+ * Zahájí device authorization flow — vrátí device code a user code.
50
+ * Agent zobrazí user code v terminálu, člověk ho zadá v browseru.
51
+ */
52
+ export async function initiateDeviceGrant(config, tenantId) {
53
+ const res = await fetch(`${config.apiUrl}/oauth/device/authorize`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({ clientId: "claude-agent", tenantId }),
57
+ });
58
+ if (!res.ok) {
59
+ const body = await res.text();
60
+ throw new Error(`Device authorization failed: ${res.status} — ${body}`);
61
+ }
62
+ return (await res.json()).data;
63
+ }
64
+ /**
65
+ * Polluje /oauth/token dokud člověk neschválí párování.
66
+ * Vrátí access + refresh token, nebo vyhodí chybu při expiraci/zamítnutí.
67
+ */
68
+ export async function pollForDeviceGrant(config, deviceCode, interval, expiresIn, onStatus) {
69
+ const deadline = Date.now() + expiresIn * 1000;
70
+ while (Date.now() < deadline) {
71
+ await sleep(interval * 1000);
72
+ try {
73
+ const res = await fetch(`${config.apiUrl}/oauth/token`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({
77
+ grantType: "urn:ietf:params:oauth:grant-type:device_code",
78
+ deviceCode,
79
+ }),
80
+ });
81
+ if (res.ok) {
82
+ return (await res.json());
83
+ }
84
+ const error = (await res.json());
85
+ if (error.error === "authorization_pending") {
86
+ onStatus?.("Waiting for approval...");
87
+ continue;
88
+ }
89
+ if (error.error === "slow_down") {
90
+ interval = Math.min(interval + 1, 15);
91
+ onStatus?.("Slowing down polling...");
92
+ continue;
93
+ }
94
+ if (error.error === "expired_token") {
95
+ throw new Error("Device code expired — please try pairing again");
96
+ }
97
+ if (error.error === "access_denied") {
98
+ throw new Error("Pairing denied by user");
99
+ }
100
+ throw new Error(`Token exchange error: ${error.error} — ${error.error_description}`);
101
+ }
102
+ catch (err) {
103
+ if (err instanceof Error && (err.message.includes("expired") || err.message.includes("denied"))) {
104
+ throw err;
105
+ }
106
+ // Síťová chyba — pokračuj v pollingu
107
+ onStatus?.("Connection error, retrying...");
108
+ }
109
+ }
110
+ throw new Error("Device code expired — please try pairing again");
111
+ }
112
+ /**
113
+ * Obnoví access token přes refresh token grant.
114
+ * Vrátí nový token pár (rotace — starý refresh token je zneplatněn).
115
+ */
116
+ export async function refreshAccessToken(config, refreshToken) {
117
+ const res = await fetch(`${config.apiUrl}/oauth/token`, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({
121
+ grantType: "refresh_token",
122
+ refreshToken,
123
+ }),
124
+ });
125
+ if (!res.ok) {
126
+ const body = await res.text();
127
+ throw new Error(`Token refresh failed: ${res.status} — ${body}`);
128
+ }
129
+ return (await res.json());
130
+ }
131
+ // --- Integrační funkce ---
132
+ /**
133
+ * Hlavní entry point: získej platný access token.
134
+ * 1. Pokud existuje uložená session a token je platný → vrať
135
+ * 2. Pokud token expiruje → auto-refresh
136
+ * 3. Pokud refresh selže → spusť device grant flow
137
+ */
138
+ export async function getDeviceGrantToken(config, agentName, tenantId, onPairingRequired) {
139
+ // Zkus načíst existující session
140
+ let session = loadSession(agentName);
141
+ if (session) {
142
+ // Token ještě platný (30s buffer)
143
+ if (session.expiresAt - Date.now() > 30_000) {
144
+ return session.accessToken;
145
+ }
146
+ // Zkus refresh
147
+ try {
148
+ const result = await refreshAccessToken(config, session.refreshToken);
149
+ session = {
150
+ accessToken: result.access_token,
151
+ refreshToken: result.refresh_token,
152
+ expiresAt: Date.now() + result.expires_in * 1000,
153
+ agentIdentity: result.agent_identity,
154
+ apiUrl: config.apiUrl,
155
+ tenantId,
156
+ };
157
+ saveSession(agentName, session);
158
+ return session.accessToken;
159
+ }
160
+ catch {
161
+ // Refresh selhal — session expirovala, potřebujeme nové párování
162
+ clearSession(agentName);
163
+ }
164
+ }
165
+ // Žádná platná session — spusť device grant flow
166
+ const deviceResponse = await initiateDeviceGrant(config, tenantId);
167
+ // Oznám uživateli
168
+ if (onPairingRequired) {
169
+ onPairingRequired(deviceResponse.user_code, deviceResponse.verification_uri);
170
+ }
171
+ else {
172
+ console.error(`[tickr] Agent is not paired.`);
173
+ console.error(`[tickr] Open ${deviceResponse.verification_uri}`);
174
+ console.error(`[tickr] Enter code: ${deviceResponse.user_code}`);
175
+ console.error(`[tickr] Waiting for approval... (expires in ${Math.round(deviceResponse.expires_in / 60)} min)`);
176
+ }
177
+ const tokenResponse = await pollForDeviceGrant(config, deviceResponse.device_code, deviceResponse.interval, deviceResponse.expires_in, (msg) => console.error(`[tickr] ${msg}`));
178
+ // Ulož session
179
+ const newSession = {
180
+ accessToken: tokenResponse.access_token,
181
+ refreshToken: tokenResponse.refresh_token,
182
+ expiresAt: Date.now() + tokenResponse.expires_in * 1000,
183
+ agentIdentity: tokenResponse.agent_identity,
184
+ apiUrl: config.apiUrl,
185
+ tenantId,
186
+ };
187
+ saveSession(agentName, newSession);
188
+ console.error(`[tickr] ✓ Paired as ${tokenResponse.agent_identity}`);
189
+ return newSession.accessToken;
190
+ }
191
+ function sleep(ms) {
192
+ return new Promise((resolve) => setTimeout(resolve, ms));
193
+ }
194
+ //# sourceMappingURL=device-grant.js.map
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { loadConfig } from "./config.js";
4
+ import { initDeviceGrantSession } from "./auth.js";
4
5
  import { ApiClient } from "./api-client.js";
5
6
  import { debugLog, isDebugEnabled } from "./debug-logger.js";
6
7
  // Tools
@@ -72,6 +73,9 @@ import { registerUpdateCycle } from "./tools/update-cycle.js";
72
73
  import { registerDeleteCycle } from "./tools/delete-cycle.js";
73
74
  import { registerUpdateEpic } from "./tools/update-epic.js";
74
75
  import { registerDeleteEpic } from "./tools/delete-epic.js";
76
+ // ADR-0050: Epic Dependencies
77
+ import { registerAddEpicDependency } from "./tools/add-epic-dependency.js";
78
+ import { registerRemoveEpicDependency } from "./tools/remove-epic-dependency.js";
75
79
  // ADR-0026 Phase 2f: Project Members
76
80
  import { registerListProjectMembers } from "./tools/list-project-members.js";
77
81
  import { registerAddProjectMember } from "./tools/add-project-member.js";
@@ -95,10 +99,20 @@ import { registerPendingTasksResource } from "./resources/pending-tasks-resource
95
99
  import { initSignalRClient, stopSignalRClient } from "./signalr-client.js";
96
100
  export async function startServer() {
97
101
  const config = loadConfig();
102
+ // Inicializace device grant session (pokud je konfigurována)
103
+ initDeviceGrantSession(config);
98
104
  const api = new ApiClient(config);
105
+ // Startup health check — ověření dostupnosti API
106
+ const healthy = await api.healthCheck();
107
+ if (healthy) {
108
+ console.error(`[tickr-mcp] API reachable at ${config.apiUrl}`);
109
+ }
110
+ else {
111
+ console.error(`[tickr-mcp] WARNING: API unreachable at ${config.apiUrl} — tools may fail`);
112
+ }
99
113
  const server = new McpServer({
100
114
  name: "tickr",
101
- version: "0.1.9",
115
+ version: "1.23.0",
102
116
  });
103
117
  // Debug logging wrapper (dedup odstraněn — nefunkční cross-process, řeší se na API straně: TKR-ADR-0043)
104
118
  {
@@ -195,6 +209,9 @@ export async function startServer() {
195
209
  registerDeleteCycle(server, api);
196
210
  registerUpdateEpic(server, api);
197
211
  registerDeleteEpic(server, api);
212
+ // ADR-0050: Epic Dependencies
213
+ registerAddEpicDependency(server, api);
214
+ registerRemoveEpicDependency(server, api);
198
215
  // ADR-0026 Phase 2f: Project Members
199
216
  registerListProjectMembers(server, api);
200
217
  registerAddProjectMember(server, api);
@@ -221,10 +238,14 @@ export async function startServer() {
221
238
  // Neni kriticke — MCP server funguje i bez SignalR
222
239
  initSignalRClient(config, server, config.defaultProject).catch(() => { });
223
240
  // Initial heartbeat — nastav agenta jako idle
224
- api.post("/api/dev-queue/heartbeat", {}).catch(() => { });
241
+ api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
242
+ console.error("[tickr-mcp] initial heartbeat failed:", err instanceof Error ? err.message : String(err));
243
+ });
225
244
  // Periodicky heartbeat kazdych 60 sekund — udržuje agent status + snižuje latenci task discovery
226
245
  const heartbeatInterval = setInterval(() => {
227
- api.post("/api/dev-queue/heartbeat", {}).catch(() => { });
246
+ api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
247
+ console.error("[tickr-mcp] heartbeat failed:", err instanceof Error ? err.message : String(err));
248
+ });
228
249
  }, 60_000);
229
250
  // Cleanup pri ukonceni procesu
230
251
  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.23.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",