mujvykaz-mcp 1.0.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ var API_KEY = process.env.MUJVYKAZ_API_KEY || process.env.CREWLOG_API_KEY;
8
+ var BASE_URL = (process.env.MUJVYKAZ_BASE_URL || process.env.CREWLOG_BASE_URL || "https://app.mujvykaz.cz").replace(/\/$/, "");
9
+ if (!API_KEY) {
10
+ console.error("MUJVYKAZ_API_KEY environment variable is required");
11
+ console.error("");
12
+ console.error(" 1. Go to app.mujvykaz.cz \u2192 Settings \u2192 API kl\xED\u010De");
13
+ console.error(" 2. Generate a personal API key");
14
+ console.error(" 3. Set MUJVYKAZ_API_KEY=cl_your_key_here");
15
+ process.exit(1);
16
+ }
17
+ var CURRENCY_SCALE = { CZK: 1, EUR: 100 };
18
+ function formatAmount(minorUnits, currency = "CZK") {
19
+ const value = minorUnits / CURRENCY_SCALE[currency];
20
+ if (currency === "CZK") return `${value.toLocaleString("cs-CZ")} K\u010D`;
21
+ return `${value.toLocaleString("cs-CZ", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} \u20AC`;
22
+ }
23
+ async function api(path, options) {
24
+ const res = await fetch(`${BASE_URL}/api/v1${path}`, {
25
+ method: options?.method ?? "GET",
26
+ headers: {
27
+ Authorization: `Bearer ${API_KEY}`,
28
+ "Content-Type": "application/json"
29
+ },
30
+ body: options?.body ? JSON.stringify(options.body) : void 0
31
+ });
32
+ if (!res.ok) {
33
+ const error = await res.json().catch(() => ({ error: res.statusText }));
34
+ throw new Error(`API error ${res.status}: ${JSON.stringify(error)}`);
35
+ }
36
+ const json = await res.json();
37
+ return json.data;
38
+ }
39
+ function formatDuration(minutes) {
40
+ const h = Math.floor(minutes / 60);
41
+ const m = minutes % 60;
42
+ if (h === 0) return `${m}m`;
43
+ if (m === 0) return `${h}h`;
44
+ return `${h}h ${m}m`;
45
+ }
46
+ var server = new McpServer({
47
+ name: "mujvykaz",
48
+ version: "1.0.0"
49
+ });
50
+ server.tool(
51
+ "my_profile",
52
+ "Zobrazit m\u016Fj profil \u2014 jm\xE9no, email, roli, pozici, hodinovou sazbu.",
53
+ {},
54
+ async () => {
55
+ const user = await api("/users/me");
56
+ return {
57
+ content: [{
58
+ type: "text",
59
+ text: `${user.name} (${user.email})
60
+ Role: ${user.role}
61
+ Pozice: ${user.position ?? "-"}
62
+ Sazba: ${formatAmount(user.hourlyRate)}/h
63
+ ID: ${user.id}`
64
+ }]
65
+ };
66
+ }
67
+ );
68
+ server.tool(
69
+ "my_stats",
70
+ "Zobrazit moje statistiky \u2014 hodiny a \u010D\xE1stky za tento t\xFDden a m\u011Bs\xEDc.",
71
+ {},
72
+ async () => {
73
+ const stats = await api("/users/me/stats");
74
+ return {
75
+ content: [{
76
+ type: "text",
77
+ text: `Tento t\xFDden: ${stats.thisWeek.totalHours}h \u2014 ${formatAmount(stats.thisWeek.totalAmount)} (${stats.thisWeek.entryCount} v\xFDkaz\u016F)
78
+ Tento m\u011Bs\xEDc: ${stats.thisMonth.totalHours}h \u2014 ${formatAmount(stats.thisMonth.totalAmount)} (${stats.thisMonth.entryCount} v\xFDkaz\u016F)`
79
+ }]
80
+ };
81
+ }
82
+ );
83
+ server.tool(
84
+ "list_time_entries",
85
+ "Zobrazit v\xFDkazy pr\xE1ce s filtrov\xE1n\xEDm. Worker vid\xED jen sv\xE9, manager vid\xED sv\u016Fj t\xFDm, admin vid\xED v\u0161e.",
86
+ {
87
+ status: z.enum(["draft", "submitted", "approved", "invoiced"]).optional().describe("Filtr podle stavu"),
88
+ project_id: z.string().uuid().optional().describe("Filtr podle ID projektu"),
89
+ user_id: z.string().uuid().optional().describe("Filtr podle ID u\u017Eivatele"),
90
+ from: z.string().optional().describe("Od data (ISO 8601)"),
91
+ to: z.string().optional().describe("Do data (ISO 8601)"),
92
+ limit: z.number().min(1).max(500).optional().describe("Max po\u010Det z\xE1znam\u016F (v\xFDchoz\xED 100)")
93
+ },
94
+ async (params) => {
95
+ const query = new URLSearchParams();
96
+ if (params.status) query.set("status", params.status);
97
+ if (params.project_id) query.set("project_id", params.project_id);
98
+ if (params.user_id) query.set("user_id", params.user_id);
99
+ if (params.from) query.set("from", params.from);
100
+ if (params.to) query.set("to", params.to);
101
+ if (params.limit) query.set("limit", String(params.limit));
102
+ const entries = await api(`/time-entries?${query}`);
103
+ const text = entries.map((e) => `[${e.status}] ${e.description} \u2014 ${formatDuration(e.durationMinutes)} \u2014 ${formatAmount(e.amount)} (${e.project?.name ?? "bez projektu"}, ${e.user?.name ?? "?"}) ${new Date(e.startedAt).toLocaleDateString("cs-CZ")}`).join("\n");
104
+ return { content: [{ type: "text", text: text || "\u017D\xE1dn\xE9 v\xFDkazy nenalezeny." }] };
105
+ }
106
+ );
107
+ server.tool(
108
+ "create_time_entry",
109
+ "Vytvo\u0159it nov\xFD v\xFDkaz pr\xE1ce.",
110
+ {
111
+ userId: z.string().uuid().describe("ID u\u017Eivatele"),
112
+ projectId: z.string().uuid().optional().describe("ID projektu"),
113
+ description: z.string().describe("Popis pr\xE1ce"),
114
+ startedAt: z.string().describe("Datum a \u010Das za\u010D\xE1tku (ISO 8601)"),
115
+ durationMinutes: z.number().min(1).describe("D\xE9lka v minut\xE1ch"),
116
+ hourlyRate: z.number().optional().describe("Hodinov\xE1 sazba"),
117
+ isBillable: z.boolean().optional().describe("Fakturovateln\xE9? (v\xFDchoz\xED true)")
118
+ },
119
+ async (params) => {
120
+ const entry = await api("/time-entries", { method: "POST", body: params });
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: `Vytvo\u0159en v\xFDkaz: "${entry.description}" \u2014 ${formatDuration(entry.durationMinutes)} \u2014 ${formatAmount(entry.amount)} (ID: ${entry.id})`
125
+ }]
126
+ };
127
+ }
128
+ );
129
+ server.tool(
130
+ "submit_time_entries",
131
+ "Odeslat v\xFDkazy ke schv\xE1len\xED (draft \u2192 submitted).",
132
+ { ids: z.array(z.string().uuid()).min(1).max(100).describe("ID v\xFDkaz\u016F k odesl\xE1n\xED") },
133
+ async (params) => {
134
+ const result = await api("/time-entries/bulk-status", { method: "PUT", body: { ids: params.ids, status: "submitted" } });
135
+ return { content: [{ type: "text", text: `Odesl\xE1no ${result.updated} v\xFDkaz\u016F ke schv\xE1len\xED.` }] };
136
+ }
137
+ );
138
+ server.tool(
139
+ "approve_time_entries",
140
+ "Schv\xE1lit v\xFDkazy (submitted \u2192 approved). Vy\u017Eaduje roli manager nebo admin.",
141
+ { ids: z.array(z.string().uuid()).min(1).max(100).describe("ID v\xFDkaz\u016F ke schv\xE1len\xED") },
142
+ async (params) => {
143
+ const result = await api("/time-entries/bulk-status", { method: "PUT", body: { ids: params.ids, status: "approved" } });
144
+ return { content: [{ type: "text", text: `Schv\xE1leno ${result.updated} v\xFDkaz\u016F.` }] };
145
+ }
146
+ );
147
+ server.tool(
148
+ "update_time_entry",
149
+ "Upravit v\xFDkaz pr\xE1ce.",
150
+ {
151
+ id: z.string().uuid().describe("ID v\xFDkazu"),
152
+ description: z.string().optional().describe("Nov\xFD popis"),
153
+ durationMinutes: z.number().min(1).optional().describe("Nov\xE1 d\xE9lka v minut\xE1ch"),
154
+ hourlyRate: z.number().min(0).optional().describe("Nov\xE1 hodinov\xE1 sazba"),
155
+ isBillable: z.boolean().optional().describe("Fakturovateln\xE9?"),
156
+ status: z.enum(["draft", "submitted", "approved", "invoiced"]).optional().describe("Nov\xFD stav"),
157
+ projectId: z.string().uuid().nullable().optional().describe("Nov\xE9 ID projektu")
158
+ },
159
+ async (params) => {
160
+ const { id, ...body } = params;
161
+ const entry = await api(`/time-entries/${id}`, { method: "PUT", body });
162
+ return {
163
+ content: [{
164
+ type: "text",
165
+ text: `V\xFDkaz upraven: "${entry.description}" \u2014 ${formatDuration(entry.durationMinutes)} \u2014 ${formatAmount(entry.amount)} [${entry.status}]`
166
+ }]
167
+ };
168
+ }
169
+ );
170
+ server.tool(
171
+ "delete_time_entry",
172
+ "Smazat v\xFDkaz pr\xE1ce.",
173
+ { id: z.string().uuid().describe("ID v\xFDkazu ke smaz\xE1n\xED") },
174
+ async (params) => {
175
+ await api(`/time-entries/${params.id}`, { method: "DELETE" });
176
+ return { content: [{ type: "text", text: `V\xFDkaz ${params.id} smaz\xE1n.` }] };
177
+ }
178
+ );
179
+ server.tool(
180
+ "list_projects",
181
+ "Zobrazit projekty s informacemi o rozpo\u010Dtu.",
182
+ {
183
+ status: z.enum(["active", "paused", "done", "archived"]).optional().describe("Filtr podle stavu"),
184
+ client_id: z.string().uuid().optional().describe("Filtr podle ID klienta")
185
+ },
186
+ async (params) => {
187
+ const query = new URLSearchParams();
188
+ if (params.status) query.set("status", params.status);
189
+ if (params.client_id) query.set("client_id", params.client_id);
190
+ const projects = await api(`/projects?${query}`);
191
+ const text = projects.map((p) => {
192
+ let budget = "";
193
+ if (p.budgetHoursPct !== null) budget += ` Hodiny: ${formatDuration(p.usedMinutes)}/${p.budgetHours}h (${p.budgetHoursPct}%)`;
194
+ if (p.budgetAmountPct !== null) budget += ` Rozpo\u010Det: ${formatAmount(p.usedAmount)}/${formatAmount(p.budgetAmount)} (${p.budgetAmountPct}%)`;
195
+ return `[${p.status}] ${p.name} (${p.client?.name ?? "?"}) \u2014 ${p.entryCount} v\xFDkaz\u016F${budget}`;
196
+ }).join("\n");
197
+ return { content: [{ type: "text", text: text || "\u017D\xE1dn\xE9 projekty nenalezeny." }] };
198
+ }
199
+ );
200
+ server.tool(
201
+ "create_project",
202
+ "Vytvo\u0159it nov\xFD projekt. Vy\u017Eaduje roli admin.",
203
+ {
204
+ clientId: z.string().uuid().describe("ID klienta"),
205
+ name: z.string().describe("N\xE1zev projektu"),
206
+ description: z.string().optional().describe("Popis projektu"),
207
+ budgetHours: z.number().min(0).optional().describe("Rozpo\u010Det v hodin\xE1ch"),
208
+ budgetAmount: z.number().min(0).optional().describe("Rozpo\u010Det v K\u010D"),
209
+ hourlyRate: z.number().min(0).optional().describe("Hodinov\xE1 sazba projektu"),
210
+ deadline: z.string().optional().describe("Deadline (ISO 8601)")
211
+ },
212
+ async (params) => {
213
+ const project = await api("/projects", { method: "POST", body: params });
214
+ return { content: [{ type: "text", text: `Projekt vytvo\u0159en: "${project.name}" (ID: ${project.id})` }] };
215
+ }
216
+ );
217
+ server.tool(
218
+ "update_project",
219
+ "Upravit projekt. Vy\u017Eaduje roli admin.",
220
+ {
221
+ id: z.string().uuid().describe("ID projektu"),
222
+ name: z.string().optional().describe("Nov\xFD n\xE1zev"),
223
+ description: z.string().nullable().optional().describe("Nov\xFD popis"),
224
+ status: z.string().optional().describe("Nov\xFD stav (active/paused/done/archived)"),
225
+ budgetHours: z.number().min(0).nullable().optional().describe("Nov\xFD rozpo\u010Det v hodin\xE1ch"),
226
+ budgetAmount: z.number().min(0).nullable().optional().describe("Nov\xFD rozpo\u010Det v K\u010D"),
227
+ hourlyRate: z.number().min(0).nullable().optional().describe("Nov\xE1 hodinov\xE1 sazba"),
228
+ deadline: z.string().nullable().optional().describe("Nov\xFD deadline (ISO 8601)")
229
+ },
230
+ async (params) => {
231
+ const { id, ...body } = params;
232
+ const project = await api(`/projects/${id}`, { method: "PUT", body });
233
+ return { content: [{ type: "text", text: `Projekt upraven: "${project.name}" [${project.status}]` }] };
234
+ }
235
+ );
236
+ server.tool(
237
+ "list_clients",
238
+ "Zobrazit klienty a jejich projekty.",
239
+ {},
240
+ async () => {
241
+ const clients = await api("/clients");
242
+ const text = clients.map((c) => {
243
+ const projs = c.projects?.map((p) => `${p.name} [${p.status}]`).join(", ") ?? "\u017E\xE1dn\xE9 projekty";
244
+ return `${c.name} (${c.contactPerson ?? "?"}, ${c.contactEmail ?? "?"}) \u2014 ${projs}`;
245
+ }).join("\n");
246
+ return { content: [{ type: "text", text: text || "\u017D\xE1dn\xED klienti nenalezeni." }] };
247
+ }
248
+ );
249
+ server.tool(
250
+ "create_client",
251
+ "Vytvo\u0159it nov\xE9ho klienta. Vy\u017Eaduje roli manager nebo admin.",
252
+ {
253
+ name: z.string().describe("N\xE1zev klienta"),
254
+ contactEmail: z.string().email().optional().describe("Kontaktn\xED email"),
255
+ contactPerson: z.string().optional().describe("Kontaktn\xED osoba"),
256
+ defaultHourlyRate: z.number().min(0).optional().describe("V\xFDchoz\xED hodinov\xE1 sazba")
257
+ },
258
+ async (params) => {
259
+ const client = await api("/clients", { method: "POST", body: params });
260
+ return { content: [{ type: "text", text: `Klient vytvo\u0159en: "${client.name}" (ID: ${client.id})` }] };
261
+ }
262
+ );
263
+ server.tool(
264
+ "update_client",
265
+ "Upravit klienta.",
266
+ {
267
+ id: z.string().uuid().describe("ID klienta"),
268
+ name: z.string().optional().describe("Nov\xFD n\xE1zev"),
269
+ contactEmail: z.string().email().nullable().optional().describe("Nov\xFD kontaktn\xED email"),
270
+ contactPerson: z.string().nullable().optional().describe("Nov\xE1 kontaktn\xED osoba"),
271
+ defaultHourlyRate: z.number().min(0).nullable().optional().describe("Nov\xE1 v\xFDchoz\xED sazba"),
272
+ isActive: z.boolean().optional().describe("Aktivn\xED?")
273
+ },
274
+ async (params) => {
275
+ const { id, ...body } = params;
276
+ const client = await api(`/clients/${id}`, { method: "PUT", body });
277
+ return { content: [{ type: "text", text: `Klient upraven: "${client.name}"` }] };
278
+ }
279
+ );
280
+ server.tool(
281
+ "list_invoices",
282
+ "Zobrazit faktury se stavy a \u010D\xE1stkami.",
283
+ {
284
+ status: z.enum(["draft", "sent", "paid"]).optional().describe("Filtr podle stavu faktury"),
285
+ client_id: z.string().uuid().optional().describe("Filtr podle ID klienta")
286
+ },
287
+ async (params) => {
288
+ const query = new URLSearchParams();
289
+ if (params.status) query.set("status", params.status);
290
+ if (params.client_id) query.set("client_id", params.client_id);
291
+ const invoices = await api(`/invoices?${query}`);
292
+ const text = invoices.map((inv) => `[${inv.status}] ${inv.number ?? "bez \u010D\xEDsla"} \u2014 ${inv.client?.name ?? "?"} \u2014 ${formatAmount(inv.totalAmount)} (vystaveno: ${inv.issuedAt ? new Date(inv.issuedAt).toLocaleDateString("cs-CZ") : "-"}, splatnost: ${inv.dueAt ? new Date(inv.dueAt).toLocaleDateString("cs-CZ") : "-"})`).join("\n");
293
+ return { content: [{ type: "text", text: text || "\u017D\xE1dn\xE9 faktury nenalezeny." }] };
294
+ }
295
+ );
296
+ server.tool(
297
+ "list_users",
298
+ "Zobrazit \u010Dleny t\xFDmu s rolemi a sazbami.",
299
+ {},
300
+ async () => {
301
+ const users = await api("/users");
302
+ const text = users.map((u) => `${u.name} (${u.email}) \u2014 ${u.role} \u2014 ${formatAmount(u.hourlyRate)}/h${u.isActive ? "" : " [NEAKTIVN\xCD]"}`).join("\n");
303
+ return { content: [{ type: "text", text: text || "\u017D\xE1dn\xED \u010Dlenov\xE9 t\xFDmu nenalezeni." }] };
304
+ }
305
+ );
306
+ server.tool(
307
+ "team_stats",
308
+ "Zobrazit statistiky t\xFDmu \u2014 hodiny a \u010D\xE1stky za tento m\u011Bs\xEDc. Vy\u017Eaduje roli manager+.",
309
+ {},
310
+ async () => {
311
+ const stats = await api("/users/stats");
312
+ const text = stats.map((s) => `${s.userName} (${s.role}) \u2014 ${s.thisMonth.totalHours}h \u2014 ${formatAmount(s.thisMonth.totalAmount)} (${s.thisMonth.entryCount} v\xFDkaz\u016F)`).join("\n");
313
+ return { content: [{ type: "text", text: text || "\u017D\xE1dn\xE9 statistiky." }] };
314
+ }
315
+ );
316
+ server.tool(
317
+ "query_reports",
318
+ "Zeptat se na data p\u0159irozen\xFDm jazykem. Vy\u017Eaduje roli manager+. P\u0159\xEDklady: 'Kolik hodin tento m\u011Bs\xEDc?', 'Top 5 projekt\u016F podle obratu'.",
319
+ { query: z.string().min(1).max(1e3).describe("Dotaz v \u010De\u0161tin\u011B nebo angli\u010Dtin\u011B") },
320
+ async (params) => {
321
+ const res = await fetch(`${BASE_URL}/api/v1/ai`, {
322
+ method: "POST",
323
+ headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
324
+ body: JSON.stringify({ query: params.query })
325
+ });
326
+ if (!res.ok) {
327
+ const error = await res.json().catch(() => ({ error: res.statusText }));
328
+ return { content: [{ type: "text", text: `Chyba: ${JSON.stringify(error)}` }] };
329
+ }
330
+ const json = await res.json();
331
+ const data = json.data;
332
+ let text = data.text ?? "\u017D\xE1dn\xE1 odpov\u011B\u010F";
333
+ if (data.data && data.data.length > 0) {
334
+ const rows = data.data;
335
+ const headers = Object.keys(rows[0]);
336
+ const table = [
337
+ headers.join(" | "),
338
+ headers.map(() => "---").join(" | "),
339
+ ...rows.map((row) => headers.map((h) => String(row[h] ?? "")).join(" | "))
340
+ ].join("\n");
341
+ text += `
342
+
343
+ ${table}`;
344
+ }
345
+ return { content: [{ type: "text", text }] };
346
+ }
347
+ );
348
+ async function main() {
349
+ const transport = new StdioServerTransport();
350
+ await server.connect(transport);
351
+ console.error("M\u016Fj V\xFDkaz MCP Server v1.0.0 running on stdio");
352
+ }
353
+ main().catch((error) => {
354
+ console.error("Fatal error:", error);
355
+ process.exit(1);
356
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "mujvykaz-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Můj Výkaz — connect Claude to your timesheet data",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mujvykaz-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --target node18 --dts --clean",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.12.1",
19
+ "zod": "^3.24.4"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.5.0",
23
+ "tsup": "^8.4.0",
24
+ "typescript": "^5.8.3"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "timesheet",
29
+ "claude",
30
+ "mujvykaz",
31
+ "evidence-prace"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/pepino-nojgic/mujvykaz-mcp"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ }
40
+ }