@kaiord/mcp 4.3.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/index.js ADDED
@@ -0,0 +1,724 @@
1
+ // src/adapters/stderr-logger.ts
2
+ var createStderrLogger = () => ({
3
+ debug: (message, context) => console.error(`[DEBUG] ${message}`, context ?? ""),
4
+ info: (message, context) => console.error(`[INFO] ${message}`, context ?? ""),
5
+ warn: (message, context) => console.error(`[WARN] ${message}`, context ?? ""),
6
+ error: (message, context) => console.error(`[ERROR] ${message}`, context ?? "")
7
+ });
8
+
9
+ // src/server/create-server.ts
10
+ import { readFileSync as readFileSync3 } from "fs";
11
+ import { dirname as dirname2, join } from "path";
12
+ import { fileURLToPath } from "url";
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+
15
+ // src/prompts/analyze-workout.ts
16
+ import { z } from "zod";
17
+ var promptArgs = {
18
+ file_path: z.string().describe("Path to the fitness file to analyze")
19
+ };
20
+ var registerAnalyzeWorkoutPrompt = (server) => {
21
+ server.prompt(
22
+ "analyze_workout",
23
+ "Guide analysis of a structured workout file",
24
+ promptArgs,
25
+ (args) => ({
26
+ messages: [
27
+ {
28
+ role: "user",
29
+ content: {
30
+ type: "text",
31
+ text: [
32
+ `Analyze the workout in the file at "${args.file_path}".`,
33
+ "",
34
+ "Steps:",
35
+ "1. Use kaiord_inspect to get an overview of the file.",
36
+ "2. Use kaiord_extract_workout to get the structured workout.",
37
+ "3. Summarize: sport, total steps, intensity distribution,",
38
+ " duration breakdown, and target zones.",
39
+ "4. Provide training recommendations if applicable."
40
+ ].join("\n")
41
+ }
42
+ }
43
+ ]
44
+ })
45
+ );
46
+ };
47
+
48
+ // src/prompts/convert-file.ts
49
+ import { z as z3 } from "zod";
50
+
51
+ // src/types/tool-schemas.ts
52
+ import { z as z2 } from "zod";
53
+ var formatSchema = z2.enum(["fit", "tcx", "zwo", "gcn", "krd"]);
54
+ var BINARY_FORMATS = /* @__PURE__ */ new Set(["fit"]);
55
+ var isBinaryFormat = (format) => BINARY_FORMATS.has(format);
56
+
57
+ // src/prompts/convert-file.ts
58
+ var promptArgs2 = {
59
+ file_path: z3.string().describe("Path to the fitness file to convert"),
60
+ target_format: formatSchema.describe("Target output format")
61
+ };
62
+ var registerConvertFilePrompt = (server) => {
63
+ server.prompt(
64
+ "convert_file",
65
+ "Guide conversion of a fitness file to another format",
66
+ promptArgs2,
67
+ (args) => ({
68
+ messages: [
69
+ {
70
+ role: "user",
71
+ content: {
72
+ type: "text",
73
+ text: [
74
+ `Convert the fitness file at "${args.file_path}" to ${args.target_format} format.`,
75
+ "",
76
+ "Steps:",
77
+ "1. Use kaiord_inspect to examine the file first.",
78
+ "2. Use kaiord_convert with the file path and target format.",
79
+ "3. Report the result and any issues."
80
+ ].join("\n")
81
+ }
82
+ }
83
+ ]
84
+ })
85
+ );
86
+ };
87
+
88
+ // src/resources/krd-format-spec.ts
89
+ import { existsSync, readFileSync } from "fs";
90
+ import { resolve } from "path";
91
+ var SPEC_URI = "kaiord://docs/krd-format";
92
+ var DOCS_URL = "https://github.com/pablo-albaladejo/kaiord/blob/main/docs/krd-format.md";
93
+ var findSpecFile = () => {
94
+ const candidates = [
95
+ resolve(process.cwd(), "docs/krd-format.md"),
96
+ resolve(process.cwd(), "packages/mcp/docs/krd-format.md")
97
+ ];
98
+ for (const path of candidates) {
99
+ if (existsSync(path)) {
100
+ return readFileSync(path, "utf-8");
101
+ }
102
+ }
103
+ return `KRD format specification not found locally. See: ${DOCS_URL}`;
104
+ };
105
+ var registerKrdFormatSpecResource = (server) => {
106
+ const spec = findSpecFile();
107
+ server.resource(
108
+ "krd-format-spec",
109
+ SPEC_URI,
110
+ { description: "KRD format specification document" },
111
+ async () => ({
112
+ contents: [{ uri: SPEC_URI, mimeType: "text/markdown", text: spec }]
113
+ })
114
+ );
115
+ };
116
+
117
+ // src/resources/krd-schema.ts
118
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
119
+ import { resolve as resolve2 } from "path";
120
+ var SCHEMA_URI = "kaiord://schema/krd";
121
+ var FALLBACK_URL = "https://github.com/pablo-albaladejo/kaiord";
122
+ var findSchemaFile = () => {
123
+ const candidates = [
124
+ resolve2(process.cwd(), "packages/core/schema/krd.json"),
125
+ resolve2(process.cwd(), "node_modules/@kaiord/core/schema/krd.json")
126
+ ];
127
+ for (const path of candidates) {
128
+ if (existsSync2(path)) {
129
+ return readFileSync2(path, "utf-8");
130
+ }
131
+ }
132
+ return JSON.stringify({
133
+ error: `KRD schema file not found. See ${FALLBACK_URL}`
134
+ });
135
+ };
136
+ var registerKrdSchemaResource = (server) => {
137
+ const schema = findSchemaFile();
138
+ server.resource(
139
+ "krd-schema",
140
+ SCHEMA_URI,
141
+ {
142
+ description: "JSON Schema for the KRD canonical format",
143
+ mimeType: "application/schema+json"
144
+ },
145
+ async () => ({
146
+ contents: [
147
+ {
148
+ uri: SCHEMA_URI,
149
+ mimeType: "application/schema+json",
150
+ text: schema
151
+ }
152
+ ]
153
+ })
154
+ );
155
+ };
156
+
157
+ // src/utils/format-registry.ts
158
+ import { validateKrd } from "@kaiord/core";
159
+ import { createFitReader, createFitWriter } from "@kaiord/fit";
160
+ import { createGarminReader, createGarminWriter } from "@kaiord/garmin";
161
+ import { createTcxReader, createTcxWriter } from "@kaiord/tcx";
162
+ import { createZwiftReader, createZwiftWriter } from "@kaiord/zwo";
163
+ import { extname } from "path";
164
+ var createKrdReader = () => async (text) => validateKrd(JSON.parse(text));
165
+ var createKrdWriter = () => async (krd) => {
166
+ validateKrd(krd);
167
+ return JSON.stringify(krd, null, 2);
168
+ };
169
+ var FORMAT_REGISTRY = {
170
+ fit: {
171
+ name: "FIT",
172
+ extension: ".fit",
173
+ description: "Garmin FIT binary protocol",
174
+ binary: true,
175
+ createReader: (l) => createFitReader(l),
176
+ createWriter: (l) => createFitWriter(l)
177
+ },
178
+ tcx: {
179
+ name: "TCX",
180
+ extension: ".tcx",
181
+ description: "Training Center XML",
182
+ binary: false,
183
+ createReader: (l) => createTcxReader(l),
184
+ createWriter: (l) => createTcxWriter(l)
185
+ },
186
+ zwo: {
187
+ name: "ZWO",
188
+ extension: ".zwo",
189
+ description: "Zwift workout XML",
190
+ binary: false,
191
+ createReader: (l) => createZwiftReader(l),
192
+ createWriter: (l) => createZwiftWriter(l)
193
+ },
194
+ gcn: {
195
+ name: "GCN",
196
+ extension: ".gcn",
197
+ description: "Garmin Connect workout JSON",
198
+ binary: false,
199
+ createReader: (l) => createGarminReader(l),
200
+ createWriter: (l) => createGarminWriter(l)
201
+ },
202
+ krd: {
203
+ name: "KRD",
204
+ extension: ".krd",
205
+ description: "Kaiord canonical JSON format",
206
+ binary: false,
207
+ createReader: () => createKrdReader(),
208
+ createWriter: () => createKrdWriter()
209
+ }
210
+ };
211
+ var detectFormatFromPath = (filePath) => {
212
+ const ext = extname(filePath).toLowerCase();
213
+ if (!ext) return null;
214
+ const entry = Object.entries(FORMAT_REGISTRY).find(
215
+ ([, desc]) => desc.extension === ext
216
+ );
217
+ return entry ? entry[0] : null;
218
+ };
219
+
220
+ // src/resources/supported-formats.ts
221
+ var FORMATS_URI = "kaiord://formats";
222
+ var registerSupportedFormatsResource = (server) => {
223
+ const data = Object.entries(FORMAT_REGISTRY).map(([key, desc]) => ({
224
+ format: key,
225
+ name: desc.name,
226
+ extension: desc.extension,
227
+ description: desc.description,
228
+ binary: desc.binary
229
+ }));
230
+ const text = JSON.stringify(data, null, 2);
231
+ server.resource(
232
+ "supported-formats",
233
+ FORMATS_URI,
234
+ { description: "All supported fitness data formats and capabilities" },
235
+ async () => ({
236
+ contents: [{ uri: FORMATS_URI, mimeType: "application/json", text }]
237
+ })
238
+ );
239
+ };
240
+
241
+ // src/tools/kaiord-convert.ts
242
+ import { z as z4 } from "zod";
243
+
244
+ // src/utils/error-formatter.ts
245
+ var formatSuccess = (text) => ({
246
+ content: [{ type: "text", text }]
247
+ });
248
+ var formatError = (error) => {
249
+ const message = error instanceof Error ? error.message : String(error);
250
+ return {
251
+ content: [{ type: "text", text: `Error: ${message}` }],
252
+ isError: true
253
+ };
254
+ };
255
+
256
+ // src/utils/file-io.ts
257
+ import {
258
+ mkdir,
259
+ readFile as fsReadFile,
260
+ writeFile as fsWriteFile
261
+ } from "fs/promises";
262
+ import { dirname } from "path";
263
+ var DANGEROUS_CHARS = /[\0|;&`$(){}!\n\r]/;
264
+ var PATH_TRAVERSAL = /(?:^|[/\\])\.\.(?:[/\\]|$)/;
265
+ var validatePathSecurity = (inputPath) => {
266
+ if (DANGEROUS_CHARS.test(inputPath)) {
267
+ throw new Error(
268
+ `Invalid path: dangerous characters detected in ${inputPath}`
269
+ );
270
+ }
271
+ if (PATH_TRAVERSAL.test(inputPath)) {
272
+ throw new Error(
273
+ `Invalid path: directory traversal detected in ${inputPath}`
274
+ );
275
+ }
276
+ };
277
+ var readFileAsBuffer = async (path) => {
278
+ validatePathSecurity(path);
279
+ try {
280
+ const buffer = await fsReadFile(path);
281
+ return new Uint8Array(buffer);
282
+ } catch (error) {
283
+ throw createFileError(path, error);
284
+ }
285
+ };
286
+ var readFileAsText = async (path) => {
287
+ validatePathSecurity(path);
288
+ try {
289
+ return await fsReadFile(path, "utf-8");
290
+ } catch (error) {
291
+ throw createFileError(path, error);
292
+ }
293
+ };
294
+ var writeOutputFile = async (path, data) => {
295
+ validatePathSecurity(path);
296
+ await mkdir(dirname(path), { recursive: true });
297
+ if (data instanceof Uint8Array) {
298
+ await fsWriteFile(path, data);
299
+ } else {
300
+ await fsWriteFile(path, data, "utf-8");
301
+ }
302
+ };
303
+ var isNodeError = (e) => e instanceof Error && "code" in e;
304
+ var createFileError = (path, error) => {
305
+ if (isNodeError(error)) {
306
+ if (error.code === "ENOENT") return new Error(`File not found: ${path}`);
307
+ if (error.code === "EACCES") return new Error(`Permission denied: ${path}`);
308
+ }
309
+ return new Error(`Failed to read file: ${path}`);
310
+ };
311
+
312
+ // src/utils/resolve-input.ts
313
+ var validateExclusiveInput = (inputFile, inputContent) => {
314
+ if (inputFile === void 0 && inputContent === void 0) {
315
+ throw new Error("Provide either input_file or input_content");
316
+ }
317
+ if (inputFile && inputContent) {
318
+ throw new Error("Provide only one of input_file or input_content");
319
+ }
320
+ };
321
+ var resolveTextInput = async (inputFile, inputContent) => {
322
+ validateExclusiveInput(inputFile, inputContent);
323
+ if (inputFile) {
324
+ return readFileAsText(inputFile);
325
+ }
326
+ return inputContent;
327
+ };
328
+
329
+ // src/tools/convert-from-krd.ts
330
+ import { toBinary, toText } from "@kaiord/core";
331
+ var convertFromKrd = async (krd, outputFormat, outputFile, logger) => {
332
+ if (isBinaryFormat(outputFormat)) {
333
+ return writeBinaryOutput(krd, outputFormat, outputFile, logger);
334
+ }
335
+ return writeTextOutput(krd, outputFormat, outputFile, logger);
336
+ };
337
+ var writeBinaryOutput = async (krd, format, outputFile, logger) => {
338
+ if (!outputFile) {
339
+ throw new Error("output_file is required for binary format (FIT)");
340
+ }
341
+ const writer = FORMAT_REGISTRY[format].createWriter(logger);
342
+ const buffer = await toBinary(krd, writer, logger);
343
+ await writeOutputFile(outputFile, buffer);
344
+ return {
345
+ content: `Binary file written (${buffer.length} bytes)`,
346
+ writtenTo: outputFile
347
+ };
348
+ };
349
+ var writeTextOutput = async (krd, format, outputFile, logger) => {
350
+ const writer = FORMAT_REGISTRY[format].createWriter(logger);
351
+ const text = await toText(krd, writer, logger);
352
+ if (outputFile) {
353
+ await writeOutputFile(outputFile, text);
354
+ }
355
+ return { content: text, writtenTo: outputFile ?? null };
356
+ };
357
+
358
+ // src/tools/convert-to-krd.ts
359
+ import { fromBinary, fromText } from "@kaiord/core";
360
+
361
+ // src/utils/base64.ts
362
+ var BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
363
+ var decodeBase64 = (input) => {
364
+ const trimmed = input.replace(/\s/g, "");
365
+ if (!BASE64_PATTERN.test(trimmed)) {
366
+ throw new Error(
367
+ "Invalid base64 content. Ensure binary file data is base64-encoded."
368
+ );
369
+ }
370
+ const buffer = Buffer.from(trimmed, "base64");
371
+ if (buffer.length === 0 && trimmed.length > 0) {
372
+ throw new Error("Base64 decoding produced empty buffer.");
373
+ }
374
+ return new Uint8Array(buffer);
375
+ };
376
+
377
+ // src/tools/convert-to-krd.ts
378
+ var convertToKrd = async (inputFile, inputContent, inputFormat, logger) => {
379
+ const format = resolveInputFormat(inputFile, inputContent, inputFormat);
380
+ if (isBinaryFormat(format)) {
381
+ return readBinaryInput(inputFile, inputContent, format, logger);
382
+ }
383
+ return readTextInput(inputFile, inputContent, format, logger);
384
+ };
385
+ var resolveInputFormat = (inputFile, inputContent, inputFormat) => {
386
+ if (inputFormat) return inputFormat;
387
+ if (inputFile) {
388
+ const detected = detectFormatFromPath(inputFile);
389
+ if (detected) return detected;
390
+ }
391
+ throw new Error("Cannot detect format. Provide input_format explicitly.");
392
+ };
393
+ var readBinaryInput = async (inputFile, inputContent, format, logger) => {
394
+ const reader = FORMAT_REGISTRY[format].createReader(logger);
395
+ if (!inputFile && inputContent === void 0) {
396
+ throw new Error("Provide either input_file or input_content");
397
+ }
398
+ const buffer = inputFile ? await readFileAsBuffer(inputFile) : decodeBase64(inputContent);
399
+ return fromBinary(buffer, reader, logger);
400
+ };
401
+ var readTextInput = async (inputFile, inputContent, format, logger) => {
402
+ const reader = FORMAT_REGISTRY[format].createReader(logger);
403
+ if (!inputFile && inputContent === void 0) {
404
+ throw new Error("Provide either input_file or input_content");
405
+ }
406
+ const text = inputFile ? await readFileAsText(inputFile) : inputContent;
407
+ return fromText(text, reader, logger);
408
+ };
409
+
410
+ // src/tools/kaiord-convert.ts
411
+ var convertSchema = {
412
+ input_file: z4.string().optional().describe("Path to input file"),
413
+ input_content: z4.string().optional().describe("Inline content (text or base64 for binary)"),
414
+ input_format: formatSchema.optional().describe("Input format (auto-detected from extension)"),
415
+ output_format: formatSchema.describe("Target output format"),
416
+ output_file: z4.string().optional().describe("Path to write output (required for FIT)")
417
+ };
418
+ var registerConvertTool = (server, logger) => {
419
+ server.tool(
420
+ "kaiord_convert",
421
+ "Convert between fitness data formats (FIT, TCX, ZWO, GCN, KRD)",
422
+ convertSchema,
423
+ async (args) => {
424
+ try {
425
+ validateExclusiveInput(args.input_file, args.input_content);
426
+ const krd = await convertToKrd(
427
+ args.input_file,
428
+ args.input_content,
429
+ args.input_format,
430
+ logger
431
+ );
432
+ const result = await convertFromKrd(
433
+ krd,
434
+ args.output_format,
435
+ args.output_file,
436
+ logger
437
+ );
438
+ const message = result.writtenTo ? `Converted to ${args.output_format}. Written to: ${result.writtenTo}
439
+
440
+ ${result.content}` : result.content;
441
+ return formatSuccess(message);
442
+ } catch (error) {
443
+ return formatError(error);
444
+ }
445
+ }
446
+ );
447
+ };
448
+
449
+ // src/tools/kaiord-diff.ts
450
+ import { z as z5 } from "zod";
451
+
452
+ // src/tools/diff-compare.ts
453
+ var METADATA_FIELDS = [
454
+ "created",
455
+ "manufacturer",
456
+ "product",
457
+ "sport",
458
+ "subSport",
459
+ "serialNumber"
460
+ ];
461
+ var compareKrdFiles = (krd1, krd2) => ({
462
+ metadata: compareMetadata(krd1, krd2),
463
+ steps: compareSteps(krd1, krd2),
464
+ extensions: compareExtensions(krd1, krd2)
465
+ });
466
+ var isDifferent = (a, b) => {
467
+ if (a === b) return false;
468
+ if (a == null || b == null) return true;
469
+ if (typeof a === "object" && typeof b === "object") {
470
+ return JSON.stringify(a) !== JSON.stringify(b);
471
+ }
472
+ return true;
473
+ };
474
+ var compareMetadata = (krd1, krd2) => {
475
+ const diffs = [];
476
+ for (const field of METADATA_FIELDS) {
477
+ const v1 = krd1.metadata[field];
478
+ const v2 = krd2.metadata[field];
479
+ if (isDifferent(v1, v2)) {
480
+ diffs.push({ field, file1: v1, file2: v2 });
481
+ }
482
+ }
483
+ return diffs;
484
+ };
485
+ var getWorkoutSteps = (krd) => {
486
+ const ext = krd.extensions?.structured_workout;
487
+ if (!ext || typeof ext !== "object" || Array.isArray(ext)) return [];
488
+ const w = ext;
489
+ return Array.isArray(w.steps) ? w.steps : [];
490
+ };
491
+ var compareSteps = (krd1, krd2) => {
492
+ const s1 = getWorkoutSteps(krd1);
493
+ const s2 = getWorkoutSteps(krd2);
494
+ const diffs = [];
495
+ const max = Math.max(s1.length, s2.length);
496
+ for (let i = 0; i < max; i++) {
497
+ if (isDifferent(s1[i], s2[i])) {
498
+ diffs.push({ field: `step[${i}]`, file1: s1[i], file2: s2[i] });
499
+ }
500
+ }
501
+ return { file1Count: s1.length, file2Count: s2.length, diffs };
502
+ };
503
+ var compareExtensions = (krd1, krd2) => {
504
+ const e1 = krd1.extensions ?? {};
505
+ const e2 = krd2.extensions ?? {};
506
+ const keys1 = Object.keys(e1);
507
+ const keys2 = Object.keys(e2);
508
+ const allKeys = /* @__PURE__ */ new Set([...keys1, ...keys2]);
509
+ const diffs = [];
510
+ for (const key of allKeys) {
511
+ if (isDifferent(e1[key], e2[key])) {
512
+ diffs.push({ field: key, file1: e1[key], file2: e2[key] });
513
+ }
514
+ }
515
+ return { file1Keys: keys1, file2Keys: keys2, diffs };
516
+ };
517
+
518
+ // src/tools/kaiord-diff.ts
519
+ var diffSchema = {
520
+ file1: z5.string().describe("Path to the first fitness data file"),
521
+ file2: z5.string().describe("Path to the second fitness data file"),
522
+ format1: formatSchema.optional().describe("Format of file1 (auto-detected from extension)"),
523
+ format2: formatSchema.optional().describe("Format of file2 (auto-detected from extension)")
524
+ };
525
+ var registerDiffTool = (server, logger) => {
526
+ server.tool(
527
+ "kaiord_diff",
528
+ "Compare two fitness files and show differences",
529
+ diffSchema,
530
+ async (args) => {
531
+ try {
532
+ const [krd1, krd2] = await Promise.all([
533
+ convertToKrd(args.file1, void 0, args.format1, logger),
534
+ convertToKrd(args.file2, void 0, args.format2, logger)
535
+ ]);
536
+ const diff = compareKrdFiles(krd1, krd2);
537
+ return formatSuccess(JSON.stringify(diff, null, 2));
538
+ } catch (error) {
539
+ return formatError(error);
540
+ }
541
+ }
542
+ );
543
+ };
544
+
545
+ // src/tools/kaiord-extract-workout.ts
546
+ import { z as z6 } from "zod";
547
+ import { extractWorkout } from "@kaiord/core";
548
+ var extractWorkoutSchema = {
549
+ input_file: z6.string().optional().describe("Path to the fitness data file"),
550
+ input_content: z6.string().optional().describe("Inline content (text or base64 for binary)"),
551
+ input_format: formatSchema.optional().describe("Input format (auto-detected from extension)")
552
+ };
553
+ var registerExtractWorkoutTool = (server, logger) => {
554
+ server.tool(
555
+ "kaiord_extract_workout",
556
+ "Extract the structured workout from a fitness file as JSON",
557
+ extractWorkoutSchema,
558
+ async (args) => {
559
+ try {
560
+ validateExclusiveInput(args.input_file, args.input_content);
561
+ const krd = await convertToKrd(
562
+ args.input_file,
563
+ args.input_content,
564
+ args.input_format,
565
+ logger
566
+ );
567
+ const workout = extractWorkout(krd);
568
+ return formatSuccess(JSON.stringify(workout, null, 2));
569
+ } catch (error) {
570
+ return formatError(error);
571
+ }
572
+ }
573
+ );
574
+ };
575
+
576
+ // src/tools/kaiord-inspect.ts
577
+ import { z as z7 } from "zod";
578
+
579
+ // src/tools/build-inspect-summary.ts
580
+ var buildInspectSummary = (krd) => {
581
+ const lines = [
582
+ `Type: ${krd.type}`,
583
+ `Sport: ${krd.metadata.sport}`,
584
+ `Sub-sport: ${krd.metadata.subSport ?? "N/A"}`,
585
+ "",
586
+ "--- Metadata ---",
587
+ `Created: ${krd.metadata.created}`,
588
+ `Manufacturer: ${krd.metadata.manufacturer ?? "N/A"}`,
589
+ `Product: ${krd.metadata.product ?? "N/A"}`,
590
+ `Serial: ${krd.metadata.serialNumber ?? "N/A"}`,
591
+ "",
592
+ "--- Data ---",
593
+ `Sessions: ${krd.sessions?.length ?? 0}`,
594
+ `Laps: ${krd.laps?.length ?? 0}`,
595
+ `Records: ${krd.records?.length ?? 0}`,
596
+ `Events: ${krd.events?.length ?? 0}`
597
+ ];
598
+ const workout = extractWorkoutInfo(krd);
599
+ lines.push("");
600
+ lines.push("--- Workout ---");
601
+ if (workout) {
602
+ lines.push(`Name: ${workout.name}`);
603
+ lines.push(`Steps: ${workout.stepCount}`);
604
+ } else {
605
+ lines.push("No structured workout found.");
606
+ }
607
+ return lines.join("\n");
608
+ };
609
+ var extractWorkoutInfo = (krd) => {
610
+ const ext = krd.extensions?.structured_workout;
611
+ if (!ext || typeof ext !== "object" || Array.isArray(ext)) return null;
612
+ const w = ext;
613
+ const steps = Array.isArray(w.steps) ? w.steps : [];
614
+ return { name: String(w.name ?? "Unnamed"), stepCount: steps.length };
615
+ };
616
+
617
+ // src/tools/kaiord-inspect.ts
618
+ var inspectSchema = {
619
+ input_file: z7.string().describe("Path to the fitness data file"),
620
+ input_format: formatSchema.optional().describe("Input format (auto-detected from extension)")
621
+ };
622
+ var registerInspectTool = (server, logger) => {
623
+ server.tool(
624
+ "kaiord_inspect",
625
+ "Inspect a fitness file and return a human-readable summary",
626
+ inspectSchema,
627
+ async (args) => {
628
+ try {
629
+ const krd = await convertToKrd(
630
+ args.input_file,
631
+ void 0,
632
+ args.input_format,
633
+ logger
634
+ );
635
+ return formatSuccess(buildInspectSummary(krd));
636
+ } catch (error) {
637
+ return formatError(error);
638
+ }
639
+ }
640
+ );
641
+ };
642
+
643
+ // src/tools/kaiord-list-formats.ts
644
+ var registerListFormatsTool = (server) => {
645
+ server.tool(
646
+ "kaiord_list_formats",
647
+ "List all supported fitness data formats with capabilities",
648
+ {},
649
+ async () => {
650
+ const formats = Object.entries(FORMAT_REGISTRY).map(([key, desc]) => ({
651
+ format: key,
652
+ name: desc.name,
653
+ extension: desc.extension,
654
+ description: desc.description,
655
+ binary: desc.binary
656
+ }));
657
+ return formatSuccess(JSON.stringify(formats, null, 2));
658
+ }
659
+ );
660
+ };
661
+
662
+ // src/tools/kaiord-validate.ts
663
+ import { z as z8 } from "zod";
664
+ import { validateKrd as validateKrd2 } from "@kaiord/core";
665
+ var validateSchema = {
666
+ input_file: z8.string().optional().describe("Path to KRD JSON file"),
667
+ input_content: z8.string().optional().describe("Inline KRD JSON content")
668
+ };
669
+ var registerValidateTool = (server, logger) => {
670
+ server.tool(
671
+ "kaiord_validate",
672
+ "Validate a KRD JSON document against the schema",
673
+ validateSchema,
674
+ async (args) => {
675
+ try {
676
+ logger.debug("Validating KRD document");
677
+ const text = await resolveTextInput(
678
+ args.input_file,
679
+ args.input_content
680
+ );
681
+ const parsed = JSON.parse(text);
682
+ validateKrd2(parsed);
683
+ return formatSuccess("Valid KRD document.");
684
+ } catch (error) {
685
+ return formatError(error);
686
+ }
687
+ }
688
+ );
689
+ };
690
+
691
+ // src/server/create-server.ts
692
+ var SERVER_NAME = "kaiord-mcp";
693
+ var __dirname2 = dirname2(fileURLToPath(import.meta.url));
694
+ var pkg = JSON.parse(
695
+ readFileSync3(join(__dirname2, "../../package.json"), "utf-8")
696
+ );
697
+ var createServer = () => {
698
+ const server = new McpServer({
699
+ name: SERVER_NAME,
700
+ version: pkg.version
701
+ });
702
+ const logger = createStderrLogger();
703
+ registerListFormatsTool(server);
704
+ registerConvertTool(server, logger);
705
+ registerValidateTool(server, logger);
706
+ registerInspectTool(server, logger);
707
+ registerExtractWorkoutTool(server, logger);
708
+ registerDiffTool(server, logger);
709
+ registerKrdSchemaResource(server);
710
+ registerSupportedFormatsResource(server);
711
+ registerKrdFormatSpecResource(server);
712
+ registerConvertFilePrompt(server);
713
+ registerAnalyzeWorkoutPrompt(server);
714
+ return server;
715
+ };
716
+ export {
717
+ FORMAT_REGISTRY,
718
+ createServer,
719
+ createStderrLogger,
720
+ detectFormatFromPath,
721
+ formatError,
722
+ formatSchema,
723
+ formatSuccess
724
+ };