@k-system/tickr-mcp 1.22.0 → 1.24.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/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,9 +6,11 @@ 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
- const config = { apiUrl, apiKey, defaultProject };
13
+ const config = { apiUrl, apiKey, defaultProject, tenantId, agentName };
12
14
  validateConfig(config);
13
15
  return config;
14
16
  }
@@ -21,6 +23,8 @@ export function loadConfig() {
21
23
  apiUrl: apiUrl || file.apiUrl || "https://localhost:6001",
22
24
  apiKey: apiKey || file.apiKey || "",
23
25
  defaultProject: defaultProject || file.defaultProject,
26
+ tenantId: tenantId || file.tenantId,
27
+ agentName: agentName || file.agentName,
24
28
  };
25
29
  validateConfig(config);
26
30
  return config;
@@ -31,6 +35,8 @@ export function loadConfig() {
31
35
  apiUrl: apiUrl || "https://localhost:6001",
32
36
  apiKey: apiKey || "",
33
37
  defaultProject,
38
+ tenantId,
39
+ agentName,
34
40
  };
35
41
  validateConfig(config);
36
42
  return config;
@@ -38,8 +44,8 @@ export function loadConfig() {
38
44
  }
39
45
  /** Validace konfigurace — loguje varování pro problematické hodnoty */
40
46
  function validateConfig(config) {
41
- if (!config.apiKey) {
42
- console.error("[tickr-mcp] WARNING: No API key configured — tools will fail with 401");
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");
43
49
  }
44
50
  if (config.apiUrl && !config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("https://")) {
45
51
  console.error(`[tickr-mcp] WARNING: apiUrl "${config.apiUrl}" doesn't start with http(s)://`);
@@ -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
@@ -98,6 +99,8 @@ import { registerPendingTasksResource } from "./resources/pending-tasks-resource
98
99
  import { initSignalRClient, stopSignalRClient } from "./signalr-client.js";
99
100
  export async function startServer() {
100
101
  const config = loadConfig();
102
+ // Inicializace device grant session (pokud je konfigurována)
103
+ initDeviceGrantSession(config);
101
104
  const api = new ApiClient(config);
102
105
  // Startup health check — ověření dostupnosti API
103
106
  const healthy = await api.healthCheck();
@@ -109,7 +112,7 @@ export async function startServer() {
109
112
  }
110
113
  const server = new McpServer({
111
114
  name: "tickr",
112
- version: "1.22.0",
115
+ version: "1.24.0",
113
116
  });
114
117
  // Debug logging wrapper (dedup odstraněn — nefunkční cross-process, řeší se na API straně: TKR-ADR-0043)
115
118
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-system/tickr-mcp",
3
- "version": "1.22.0",
3
+ "version": "1.24.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",