@cosyte/cli 0.0.6 → 0.1.1
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/CHANGELOG.md +326 -2
- package/README.md +118 -53
- package/dist/bin/cosyte-mcp.cjs +461 -106
- package/dist/bin/cosyte-mcp.cjs.map +1 -1
- package/dist/bin/cosyte-mcp.mjs +461 -106
- package/dist/bin/cosyte-mcp.mjs.map +1 -1
- package/dist/bin/cosyte.cjs +824 -140
- package/dist/bin/cosyte.cjs.map +1 -1
- package/dist/bin/cosyte.mjs +825 -141
- package/dist/bin/cosyte.mjs.map +1 -1
- package/dist/index.cjs +592 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +515 -281
- package/dist/index.d.ts +515 -281
- package/dist/index.mjs +574 -104
- package/dist/index.mjs.map +1 -1
- package/dist/io-C9bSG2zF.d.cts +381 -0
- package/dist/io-C9bSG2zF.d.ts +381 -0
- package/dist/mcp.cjs +435 -103
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +70 -9
- package/dist/mcp.d.ts +70 -9
- package/dist/mcp.mjs +435 -103
- package/dist/mcp.mjs.map +1 -1
- package/package.json +30 -15
- package/dist/io-B2EobRzC.d.cts +0 -57
- package/dist/io-B2EobRzC.d.ts +0 -57
package/dist/bin/cosyte-mcp.mjs
CHANGED
|
@@ -4,28 +4,33 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { parseArgs } from 'util';
|
|
6
6
|
|
|
7
|
-
// src/core/version.ts
|
|
8
|
-
var VERSION = "0.0.6";
|
|
9
|
-
|
|
10
7
|
// src/core/exit-codes.ts
|
|
11
8
|
var EXIT = {
|
|
12
9
|
/** Success: the operation completed; `validate` found the input **valid**. */
|
|
13
10
|
OK: 0,
|
|
14
|
-
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant)
|
|
15
|
-
* a real, expected CI signal that the
|
|
16
|
-
* unparseable input (that is `DATAERR`). */
|
|
11
|
+
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant),
|
|
12
|
+
* or `redact` could not de-identify every locus: a real, expected CI signal that the tool worked
|
|
13
|
+
* and the operation did not complete. Never emitted for unparseable input (that is `DATAERR`). */
|
|
17
14
|
INVALID: 1,
|
|
18
|
-
/** Usage error: unknown command, bad flag, missing argument
|
|
15
|
+
/** Usage error: unknown command, bad flag, missing argument. A value specific to this CLI, not a
|
|
16
|
+
* `sysexits.h` constant. */
|
|
19
17
|
USAGE: 2,
|
|
20
18
|
/** Data error: input could not be parsed or its format could not be detected (`EX_DATAERR`). */
|
|
21
19
|
DATAERR: 65,
|
|
22
20
|
/** No input: the named file does not exist or is unreadable (`EX_NOINPUT`). */
|
|
23
21
|
NOINPUT: 66,
|
|
24
|
-
/** Unavailable: a required capability is not
|
|
25
|
-
*
|
|
22
|
+
/** Unavailable: a required capability is not wired for this input (e.g. `redact` on a format
|
|
23
|
+
* `@cosyte/deid` has no adapter for, or with that optional library absent), a distinct non-zero
|
|
24
|
+
* signal that is never a fake success (`EX_UNAVAILABLE`). */
|
|
26
25
|
UNAVAILABLE: 69,
|
|
27
26
|
/** Internal error: an unexpected exception (a bug), distinct from a handled bad input (`EX_SOFTWARE`). */
|
|
28
|
-
SOFTWARE: 70
|
|
27
|
+
SOFTWARE: 70,
|
|
28
|
+
/** Output error: the stream the CLI was writing to closed before the output had been delivered, so
|
|
29
|
+
* the consumer holds nothing or holds only part of the answer. The shape a downstream consumer
|
|
30
|
+
* that exits early produces (a pager quit, a `head` that has seen enough). A handled condition the
|
|
31
|
+
* consumer owns, never a defect in the CLI, so it is deliberately distinct from `SOFTWARE`
|
|
32
|
+
* (`EX_IOERR`). */
|
|
33
|
+
IOERR: 74
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
// src/core/diagnostics.ts
|
|
@@ -49,9 +54,27 @@ var CLI_CODES = {
|
|
|
49
54
|
CLI_PARSER_UNAVAILABLE: "CLI_PARSER_UNAVAILABLE",
|
|
50
55
|
/** The wrapped parser rejected the input. Positional context only, never the offending bytes. Exit `65`. */
|
|
51
56
|
CLI_PARSE_FAILED: "CLI_PARSE_FAILED",
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
57
|
+
/** The input is larger than the documented size limit the CLI reads in one invocation. Raised
|
|
58
|
+
* against the running byte count while the input is read, so the refusal always arrives before an
|
|
59
|
+
* allocation ceiling could. Names the limit, never the input. Exit `65`, never `70`. */
|
|
60
|
+
CLI_INPUT_TOO_LARGE: "CLI_INPUT_TOO_LARGE",
|
|
61
|
+
/** The output stream could not be written: a consumer that closes the pipe ends the invocation
|
|
62
|
+
* here rather than as an unhandled platform error, whether it went away part way through a record
|
|
63
|
+
* stream or before a single-result write was delivered. Names no input, and never the platform's
|
|
64
|
+
* own error text. Exit `74`. */
|
|
65
|
+
CLI_OUTPUT_WRITE_FAILED: "CLI_OUTPUT_WRITE_FAILED",
|
|
66
|
+
/** The BYO ConceptMap supplied to `map-codes` is not valid JSON or not a loadable FHIR ConceptMap.
|
|
67
|
+
* Names the stable terminology-loader code, never the map's bytes. Exit `65`. */
|
|
68
|
+
CLI_MAP_INVALID: "CLI_MAP_INVALID",
|
|
69
|
+
/** A command whose ground-layer library does not cover the resolved format (e.g. `redact` on a
|
|
70
|
+
* format `@cosyte/deid` ships no adapter for). Never a fake success: a distinct, value-free
|
|
71
|
+
* "unavailable" signal. Exit `69`. */
|
|
54
72
|
CLI_NOT_IMPLEMENTED: "CLI_NOT_IMPLEMENTED",
|
|
73
|
+
/** The delegated de-identifier reported that it could not handle every element of the input (a
|
|
74
|
+
* fail-closed `blocked` locus, or its own fatal). The run is **not** a de-identified copy, so no
|
|
75
|
+
* output is emitted: an operation-level failure, never a bug. Names loci and codes, never a
|
|
76
|
+
* value. Exit `1`. */
|
|
77
|
+
CLI_DEID_INCOMPLETE: "CLI_DEID_INCOMPLETE",
|
|
55
78
|
/** An unexpected internal error (a bug). Exit `70`. */
|
|
56
79
|
CLI_INTERNAL: "CLI_INTERNAL"
|
|
57
80
|
};
|
|
@@ -88,8 +111,12 @@ function toCliError(err) {
|
|
|
88
111
|
);
|
|
89
112
|
}
|
|
90
113
|
|
|
114
|
+
// src/core/version.ts
|
|
115
|
+
var VERSION = "0.1.1";
|
|
116
|
+
|
|
91
117
|
// src/core/format.ts
|
|
92
|
-
var
|
|
118
|
+
var DETECT_PREFIX_BYTES = 512;
|
|
119
|
+
var SNIFF_BYTES = DETECT_PREFIX_BYTES;
|
|
93
120
|
function leadingText(bytes) {
|
|
94
121
|
const slice = bytes.subarray(0, SNIFF_BYTES);
|
|
95
122
|
let text = new TextDecoder("utf-8", { fatal: false }).decode(slice);
|
|
@@ -186,6 +213,21 @@ function asCosyteFormat(value) {
|
|
|
186
213
|
return KNOWN_FORMATS.includes(value) ? value : null;
|
|
187
214
|
}
|
|
188
215
|
|
|
216
|
+
// src/core/limits.ts
|
|
217
|
+
var BYTES_PER_MIB = 1024 * 1024;
|
|
218
|
+
var MAX_INPUT_BYTES = 64 * BYTES_PER_MIB;
|
|
219
|
+
function describeByteLimit(limit = MAX_INPUT_BYTES) {
|
|
220
|
+
const mib = limit / BYTES_PER_MIB;
|
|
221
|
+
return Number.isInteger(mib) ? `${String(limit)} bytes (${String(mib)} MiB)` : `${String(limit)} bytes`;
|
|
222
|
+
}
|
|
223
|
+
function inputTooLargeError(limit = MAX_INPUT_BYTES) {
|
|
224
|
+
return new CliError(
|
|
225
|
+
CLI_CODES.CLI_INPUT_TOO_LARGE,
|
|
226
|
+
EXIT.DATAERR,
|
|
227
|
+
`input is larger than the ${describeByteLimit(limit)} this command reads in one invocation; split the input and re-run`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
189
231
|
// src/core/findings.ts
|
|
190
232
|
function formatHl7Position(pos) {
|
|
191
233
|
let out = `seg[${String(pos.segmentIndex)}]`;
|
|
@@ -492,11 +534,7 @@ async function validateFormat(format, bytes) {
|
|
|
492
534
|
}
|
|
493
535
|
async function deframeMllp(bytes) {
|
|
494
536
|
if (bytes.lastIndexOf(11) > bytes.lastIndexOf(28)) {
|
|
495
|
-
throw
|
|
496
|
-
CLI_CODES.CLI_PARSE_FAILED,
|
|
497
|
-
EXIT.DATAERR,
|
|
498
|
-
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
499
|
-
);
|
|
537
|
+
throw truncatedMllpError();
|
|
500
538
|
}
|
|
501
539
|
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
502
540
|
const payloads = [];
|
|
@@ -508,6 +546,13 @@ async function deframeMllp(bytes) {
|
|
|
508
546
|
reader.push(Buffer.from(bytes));
|
|
509
547
|
return { payloads, warningCount };
|
|
510
548
|
}
|
|
549
|
+
function truncatedMllpError() {
|
|
550
|
+
return new CliError(
|
|
551
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
552
|
+
EXIT.DATAERR,
|
|
553
|
+
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
511
556
|
function decode(bytes) {
|
|
512
557
|
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
513
558
|
}
|
|
@@ -523,6 +568,84 @@ function unsupportedInternal(format, op) {
|
|
|
523
568
|
);
|
|
524
569
|
}
|
|
525
570
|
|
|
571
|
+
// src/core/records.ts
|
|
572
|
+
var LINE_FEED = 10;
|
|
573
|
+
var CARRIAGE_RETURN = 13;
|
|
574
|
+
var VERTICAL_TAB = 11;
|
|
575
|
+
var FILE_SEPARATOR = 28;
|
|
576
|
+
function asBuffer(chunk) {
|
|
577
|
+
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.length);
|
|
578
|
+
}
|
|
579
|
+
async function* oneChunk(bytes) {
|
|
580
|
+
await Promise.resolve();
|
|
581
|
+
if (bytes.length > 0) yield bytes;
|
|
582
|
+
}
|
|
583
|
+
async function* withinLimit(chunks, limit = MAX_INPUT_BYTES) {
|
|
584
|
+
let total = 0;
|
|
585
|
+
for await (const chunk of chunks) {
|
|
586
|
+
total += chunk.length;
|
|
587
|
+
if (total > limit) throw inputTooLargeError(limit);
|
|
588
|
+
yield chunk;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async function collectChunks(chunks) {
|
|
592
|
+
const parts = [];
|
|
593
|
+
for await (const chunk of chunks) parts.push(asBuffer(chunk));
|
|
594
|
+
return Buffer.concat(parts);
|
|
595
|
+
}
|
|
596
|
+
function isBlank(line) {
|
|
597
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(line).trim().length === 0;
|
|
598
|
+
}
|
|
599
|
+
function stripCarriageReturn(line) {
|
|
600
|
+
return line.length > 0 && line[line.length - 1] === CARRIAGE_RETURN ? line.subarray(0, line.length - 1) : line;
|
|
601
|
+
}
|
|
602
|
+
async function* ndjsonRecords(chunks) {
|
|
603
|
+
let carry = Buffer.alloc(0);
|
|
604
|
+
for await (const chunk of chunks) {
|
|
605
|
+
const buf = carry.length > 0 ? Buffer.concat([carry, asBuffer(chunk)]) : asBuffer(chunk);
|
|
606
|
+
let start = 0;
|
|
607
|
+
for (; ; ) {
|
|
608
|
+
const end = buf.indexOf(LINE_FEED, start);
|
|
609
|
+
if (end < 0) break;
|
|
610
|
+
const line = stripCarriageReturn(buf.subarray(start, end));
|
|
611
|
+
if (!isBlank(line)) yield line;
|
|
612
|
+
start = end + 1;
|
|
613
|
+
}
|
|
614
|
+
carry = start === 0 ? asBuffer(buf) : Buffer.from(buf.subarray(start));
|
|
615
|
+
}
|
|
616
|
+
const last = stripCarriageReturn(carry);
|
|
617
|
+
if (!isBlank(last)) yield last;
|
|
618
|
+
}
|
|
619
|
+
async function* mllpFrames(chunks) {
|
|
620
|
+
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
621
|
+
const ready = [];
|
|
622
|
+
const reader = new FrameReader({
|
|
623
|
+
onFrame: (payload) => ready.push(Buffer.from(payload)),
|
|
624
|
+
onWarning: () => void 0,
|
|
625
|
+
// The CLI's own documented input limit is the binding one, so the reader's smaller default frame
|
|
626
|
+
// ceiling is raised to it: an over-limit MLLP stream is refused by the limit the help output and
|
|
627
|
+
// the command reference state, not by an undocumented one underneath it.
|
|
628
|
+
maxFrameSizeBytes: MAX_INPUT_BYTES
|
|
629
|
+
});
|
|
630
|
+
let lastStart = -1;
|
|
631
|
+
let lastEnd = -1;
|
|
632
|
+
let offset = 0;
|
|
633
|
+
for await (const chunk of chunks) {
|
|
634
|
+
const buf = asBuffer(chunk);
|
|
635
|
+
const start = buf.lastIndexOf(VERTICAL_TAB);
|
|
636
|
+
if (start >= 0) lastStart = offset + start;
|
|
637
|
+
const end = buf.lastIndexOf(FILE_SEPARATOR);
|
|
638
|
+
if (end >= 0) lastEnd = offset + end;
|
|
639
|
+
offset += buf.length;
|
|
640
|
+
reader.push(buf);
|
|
641
|
+
while (ready.length > 0) {
|
|
642
|
+
const frame = ready.shift();
|
|
643
|
+
if (frame !== void 0) yield frame;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (lastStart > lastEnd) throw truncatedMllpError();
|
|
647
|
+
}
|
|
648
|
+
|
|
526
649
|
// src/core/input.ts
|
|
527
650
|
async function resolveInput(source, formatOverride, deps, op) {
|
|
528
651
|
if (source === void 0) {
|
|
@@ -544,34 +667,91 @@ async function resolveInput(source, formatOverride, deps, op) {
|
|
|
544
667
|
if (bytes.length === 0) {
|
|
545
668
|
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
546
669
|
}
|
|
670
|
+
const format = resolveFormat(bytes, formatOverride, op);
|
|
671
|
+
if (format instanceof CliError) return fail(format);
|
|
672
|
+
return { ok: true, input: { format, bytes } };
|
|
673
|
+
}
|
|
674
|
+
async function resolveInputStream(source, formatOverride, deps, op, limit = MAX_INPUT_BYTES) {
|
|
675
|
+
if (source === void 0) {
|
|
676
|
+
return fail(
|
|
677
|
+
new CliError(
|
|
678
|
+
CLI_CODES.CLI_USAGE,
|
|
679
|
+
EXIT.USAGE,
|
|
680
|
+
"missing <file> argument; pass a path or `-` to read stdin"
|
|
681
|
+
)
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const iterator = withinLimit(openChunks(source, deps), limit)[Symbol.asyncIterator]();
|
|
685
|
+
const need = formatOverride === void 0 ? DETECT_PREFIX_BYTES : 1;
|
|
686
|
+
const head = [];
|
|
687
|
+
let headBytes = 0;
|
|
688
|
+
try {
|
|
689
|
+
while (headBytes < need) {
|
|
690
|
+
const step = await iterator.next();
|
|
691
|
+
if (step.done === true) break;
|
|
692
|
+
head.push(step.value);
|
|
693
|
+
headBytes += step.value.length;
|
|
694
|
+
}
|
|
695
|
+
} catch (e) {
|
|
696
|
+
if (e instanceof CliError) return fail(e);
|
|
697
|
+
throw e;
|
|
698
|
+
}
|
|
699
|
+
if (headBytes === 0) {
|
|
700
|
+
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
701
|
+
}
|
|
702
|
+
const prefix = Buffer.concat(
|
|
703
|
+
head.map((c) => Buffer.from(c.buffer, c.byteOffset, c.length)),
|
|
704
|
+
Math.min(headBytes, DETECT_PREFIX_BYTES)
|
|
705
|
+
);
|
|
706
|
+
const format = resolveFormat(prefix, formatOverride, op);
|
|
707
|
+
if (format instanceof CliError) {
|
|
708
|
+
await iterator.return(void 0);
|
|
709
|
+
return fail(format);
|
|
710
|
+
}
|
|
711
|
+
return { ok: true, input: { format, chunks: rejoin(head, iterator) } };
|
|
712
|
+
}
|
|
713
|
+
function openChunks(source, deps) {
|
|
714
|
+
if (source === "-") {
|
|
715
|
+
return deps.openStdin === void 0 ? readerAsChunks(() => deps.readStdin()) : deps.openStdin();
|
|
716
|
+
}
|
|
717
|
+
return deps.openFile === void 0 ? readerAsChunks(() => deps.readFile(source)) : deps.openFile(source);
|
|
718
|
+
}
|
|
719
|
+
async function* readerAsChunks(read) {
|
|
720
|
+
yield* oneChunk(await read());
|
|
721
|
+
}
|
|
722
|
+
async function* rejoin(head, rest) {
|
|
723
|
+
for (const chunk of head) yield chunk;
|
|
724
|
+
for (; ; ) {
|
|
725
|
+
const step = await rest.next();
|
|
726
|
+
if (step.done === true) return;
|
|
727
|
+
yield step.value;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function resolveFormat(prefix, formatOverride, op) {
|
|
547
731
|
let format;
|
|
548
732
|
if (formatOverride !== void 0) {
|
|
549
733
|
const narrowed = asCosyteFormat(formatOverride);
|
|
550
734
|
if (narrowed === null) {
|
|
551
|
-
return
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
556
|
-
)
|
|
735
|
+
return new CliError(
|
|
736
|
+
CLI_CODES.CLI_USAGE,
|
|
737
|
+
EXIT.USAGE,
|
|
738
|
+
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
557
739
|
);
|
|
558
740
|
}
|
|
559
741
|
format = narrowed;
|
|
560
742
|
} else {
|
|
561
|
-
const detected = detectFormat(
|
|
562
|
-
if (detected.format === null) return
|
|
743
|
+
const detected = detectFormat(prefix);
|
|
744
|
+
if (detected.format === null) return detectionError(detected);
|
|
563
745
|
format = detected.format;
|
|
564
746
|
}
|
|
565
|
-
if (!supportsOp(format, op)) {
|
|
566
|
-
return
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
571
|
-
)
|
|
747
|
+
if (op !== null && !supportsOp(format, op)) {
|
|
748
|
+
return new CliError(
|
|
749
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
750
|
+
EXIT.DATAERR,
|
|
751
|
+
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
572
752
|
);
|
|
573
753
|
}
|
|
574
|
-
return
|
|
754
|
+
return format;
|
|
575
755
|
}
|
|
576
756
|
function fail(e) {
|
|
577
757
|
return { ok: false, result: errorResult(e) };
|
|
@@ -843,11 +1023,24 @@ async function parseCommand(args, deps, posture = VALUE_FREE) {
|
|
|
843
1023
|
)
|
|
844
1024
|
);
|
|
845
1025
|
}
|
|
846
|
-
const resolved = await
|
|
1026
|
+
const resolved = await resolveInputStream(positionals[0], values.format, deps, "parse");
|
|
847
1027
|
if (!resolved.ok) return resolved.result;
|
|
848
|
-
const { format,
|
|
1028
|
+
const { format, chunks } = resolved.input;
|
|
849
1029
|
if (format === "mllp" || values.ndjson === true) {
|
|
850
|
-
return await parseMulti(
|
|
1030
|
+
return await parseMulti(
|
|
1031
|
+
format,
|
|
1032
|
+
chunks,
|
|
1033
|
+
values.ndjson === true,
|
|
1034
|
+
values.quiet === true,
|
|
1035
|
+
deps.writeStdout
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
let bytes;
|
|
1039
|
+
try {
|
|
1040
|
+
bytes = await collectChunks(chunks);
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1043
|
+
throw e;
|
|
851
1044
|
}
|
|
852
1045
|
return await parseSingle(format, bytes, values.json === true, values.quiet === true, posture);
|
|
853
1046
|
}
|
|
@@ -866,23 +1059,57 @@ async function parseSingle(format, bytes, json, quiet, posture) {
|
|
|
866
1059
|
` : "";
|
|
867
1060
|
return { stdout, stderr, exit: EXIT.OK };
|
|
868
1061
|
}
|
|
869
|
-
async function parseMulti(format,
|
|
870
|
-
|
|
871
|
-
|
|
1062
|
+
async function parseMulti(format, chunks, ndjson, quiet, writeStdout) {
|
|
1063
|
+
const recordFormat = format === "mllp" ? "hl7" : format;
|
|
1064
|
+
const records = format === "mllp" ? mllpFrames(chunks) : ndjsonRecords(chunks);
|
|
1065
|
+
const held = [];
|
|
1066
|
+
const emit = (line) => {
|
|
1067
|
+
if (writeStdout === void 0) {
|
|
1068
|
+
held.push(line);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
writeStdout(line);
|
|
1073
|
+
} catch {
|
|
1074
|
+
throw new CliError(
|
|
1075
|
+
CLI_CODES.CLI_OUTPUT_WRITE_FAILED,
|
|
1076
|
+
EXIT.IOERR,
|
|
1077
|
+
"could not write to the output stream; it closed before the record stream finished"
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
const written = () => held.join("");
|
|
1082
|
+
let total = 0;
|
|
1083
|
+
let failed = 0;
|
|
1084
|
+
let warnings = 0;
|
|
872
1085
|
try {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1086
|
+
for await (const record of records) {
|
|
1087
|
+
let line;
|
|
1088
|
+
try {
|
|
1089
|
+
const { model, warnings: ws } = await parseFormat(recordFormat, record);
|
|
1090
|
+
warnings += ws.length;
|
|
1091
|
+
line = { record: total, format: recordFormat, model, warnings: ws };
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
if (e instanceof CliError) throw e;
|
|
1094
|
+
failed += 1;
|
|
1095
|
+
line = {
|
|
1096
|
+
record: total,
|
|
1097
|
+
format: recordFormat,
|
|
1098
|
+
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
total += 1;
|
|
1102
|
+
emit(JSON.stringify(line) + "\n");
|
|
880
1103
|
}
|
|
881
1104
|
} catch (e) {
|
|
882
|
-
if (e instanceof CliError)
|
|
883
|
-
|
|
1105
|
+
if (e instanceof CliError) {
|
|
1106
|
+
return { stdout: written(), stderr: `${formatDiagnostic(e)}
|
|
1107
|
+
`, exit: e.exit };
|
|
1108
|
+
}
|
|
1109
|
+
const rejected = parseFailureResult(format, new Uint8Array(), VALUE_FREE, e);
|
|
1110
|
+
return { stdout: written(), stderr: rejected.stderr, exit: rejected.exit };
|
|
884
1111
|
}
|
|
885
|
-
if (
|
|
1112
|
+
if (total === 0) {
|
|
886
1113
|
return errorResult(
|
|
887
1114
|
new CliError(
|
|
888
1115
|
CLI_CODES.CLI_PARSE_FAILED,
|
|
@@ -891,34 +1118,9 @@ async function parseMulti(format, bytes, ndjson, quiet) {
|
|
|
891
1118
|
)
|
|
892
1119
|
);
|
|
893
1120
|
}
|
|
894
|
-
const lines = [];
|
|
895
|
-
let failed = 0;
|
|
896
|
-
let warnings = 0;
|
|
897
|
-
for (let i = 0; i < records.length; i += 1) {
|
|
898
|
-
const rec = records[i];
|
|
899
|
-
try {
|
|
900
|
-
const { model, warnings: ws } = await parseFormat(recordFormat, rec);
|
|
901
|
-
warnings += ws.length;
|
|
902
|
-
lines.push({ record: i, format: recordFormat, model, warnings: ws });
|
|
903
|
-
} catch (e) {
|
|
904
|
-
if (e instanceof CliError) return errorResult(e);
|
|
905
|
-
failed += 1;
|
|
906
|
-
lines.push({
|
|
907
|
-
record: i,
|
|
908
|
-
format: recordFormat,
|
|
909
|
-
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
const stdout = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
|
|
914
1121
|
const exit = failed > 0 ? EXIT.DATAERR : EXIT.OK;
|
|
915
|
-
const stderr = quiet ? "" : `cosyte: parsed ${String(
|
|
916
|
-
return { stdout, stderr, exit };
|
|
917
|
-
}
|
|
918
|
-
function splitLines(bytes) {
|
|
919
|
-
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
920
|
-
const enc = new TextEncoder();
|
|
921
|
-
return text.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => enc.encode(line));
|
|
1122
|
+
const stderr = quiet ? "" : `cosyte: parsed ${String(total)} ${recordFormat} record(s) (${String(warnings)} warning(s), ${String(failed)} failed)` + (ndjson ? " [ndjson]" : format === "mllp" ? " [mllp]" : "") + "\n";
|
|
1123
|
+
return { stdout: written(), stderr, exit };
|
|
922
1124
|
}
|
|
923
1125
|
var VALIDATE_OPTIONS = {
|
|
924
1126
|
format: { type: "string" },
|
|
@@ -1001,39 +1203,116 @@ var FORMAT_PROP = {
|
|
|
1001
1203
|
description: "Optional format override; omit to autodetect by content. A format whose parser does not support the requested operation returns a value-free CLI_FORMAT_UNSUPPORTED (never a fake)."
|
|
1002
1204
|
}
|
|
1003
1205
|
};
|
|
1206
|
+
var OUTCOME_PROPS = {
|
|
1207
|
+
ok: {
|
|
1208
|
+
type: "boolean",
|
|
1209
|
+
description: "True iff the tool call produced data. A negative verdict about the message is still true (the tool worked); only a call that produced no data at all is false."
|
|
1210
|
+
},
|
|
1211
|
+
status: {
|
|
1212
|
+
type: "string",
|
|
1213
|
+
enum: ["success", "verdict", "failed"],
|
|
1214
|
+
description: "The outcome to branch on without parsing text: 'success' the operation completed cleanly, 'verdict' the tool ran and reports a negative finding about the message, 'failed' the call produced no data (a usage mistake, unparseable input, an unavailable parser, an internal error)."
|
|
1215
|
+
},
|
|
1216
|
+
exit: {
|
|
1217
|
+
type: "integer",
|
|
1218
|
+
enum: Object.values(EXIT),
|
|
1219
|
+
description: "The exit code the underlying command resolved to, from the documented exit-code contract."
|
|
1220
|
+
},
|
|
1221
|
+
code: {
|
|
1222
|
+
type: "string",
|
|
1223
|
+
enum: Object.values(CLI_CODES),
|
|
1224
|
+
description: "The stable diagnostic code on a failed call, drawn from a fixed registry so it never carries an input value. Absent on a call that produced data."
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
var OUTCOME_REQUIRED = ["ok", "status", "exit"];
|
|
1228
|
+
function outputSchema(data) {
|
|
1229
|
+
return {
|
|
1230
|
+
type: "object",
|
|
1231
|
+
properties: { ...OUTCOME_PROPS, data },
|
|
1232
|
+
required: [...OUTCOME_REQUIRED],
|
|
1233
|
+
additionalProperties: false
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1004
1236
|
var TOOL_DEFS = [
|
|
1005
1237
|
{
|
|
1006
1238
|
name: "parse",
|
|
1239
|
+
title: "Parse a healthcare message",
|
|
1007
1240
|
description: "Parse a healthcare message (HL7 v2 or FHIR R4) to typed JSON. Format is autodetected by content. Returns the parsed model plus value-free warnings.",
|
|
1008
1241
|
inputSchema: {
|
|
1009
1242
|
type: "object",
|
|
1010
1243
|
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1011
1244
|
required: ["content"],
|
|
1012
1245
|
additionalProperties: false
|
|
1013
|
-
}
|
|
1246
|
+
},
|
|
1247
|
+
outputSchema: outputSchema({
|
|
1248
|
+
type: "object",
|
|
1249
|
+
description: "The parse payload, present on a call that produced data. A single message parses to format + model + warnings. A multi-record input (an MLLP stream) parses to `records`, one entry per record: a parsed record, or a value-free per-record error code.",
|
|
1250
|
+
properties: {
|
|
1251
|
+
format: { type: "string", description: "The format the input was parsed as." },
|
|
1252
|
+
model: { description: "The wrapped library's parsed model, verbatim: the data channel." },
|
|
1253
|
+
warnings: {
|
|
1254
|
+
type: "array",
|
|
1255
|
+
description: "Value-free parse warnings: a stable code plus positional context."
|
|
1256
|
+
},
|
|
1257
|
+
records: {
|
|
1258
|
+
type: "array",
|
|
1259
|
+
description: "One entry per record of a multi-record input, in stream order."
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
additionalProperties: false
|
|
1263
|
+
})
|
|
1014
1264
|
},
|
|
1015
1265
|
{
|
|
1016
1266
|
name: "validate",
|
|
1267
|
+
title: "Validate a healthcare message",
|
|
1017
1268
|
description: "Validate a message and carry the verdict: ok=true with a valid result, or a result reporting value-free findings. The parsed-but-invalid verdict is a successful call (not a tool error).",
|
|
1018
1269
|
inputSchema: {
|
|
1019
1270
|
type: "object",
|
|
1020
1271
|
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1021
1272
|
required: ["content"],
|
|
1022
1273
|
additionalProperties: false
|
|
1023
|
-
}
|
|
1274
|
+
},
|
|
1275
|
+
outputSchema: outputSchema({
|
|
1276
|
+
type: "object",
|
|
1277
|
+
description: "The validation payload, present on a call that produced data (a valid result and a negative verdict alike). Value-free throughout: findings are codes, severities and positional locators, never a field value.",
|
|
1278
|
+
properties: {
|
|
1279
|
+
format: { type: "string", description: "The format the input was validated as." },
|
|
1280
|
+
valid: {
|
|
1281
|
+
type: "boolean",
|
|
1282
|
+
description: "The verdict: false means parsed-but-non-conformant, not a failed call."
|
|
1283
|
+
},
|
|
1284
|
+
findings: {
|
|
1285
|
+
type: "array",
|
|
1286
|
+
description: "Value-free findings: a stable code, a severity, and a positional locator."
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
required: ["format", "valid", "findings"],
|
|
1290
|
+
additionalProperties: false
|
|
1291
|
+
})
|
|
1024
1292
|
},
|
|
1025
1293
|
{
|
|
1026
1294
|
name: "inspect",
|
|
1295
|
+
title: "Inspect a healthcare message's structure",
|
|
1027
1296
|
description: "Return a value-free structural summary of a message: its type, segment/entry counts, and a warning/issue count. Never includes a field value.",
|
|
1028
1297
|
inputSchema: {
|
|
1029
1298
|
type: "object",
|
|
1030
1299
|
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1031
1300
|
required: ["content"],
|
|
1032
1301
|
additionalProperties: false
|
|
1033
|
-
}
|
|
1302
|
+
},
|
|
1303
|
+
outputSchema: outputSchema({
|
|
1304
|
+
type: "object",
|
|
1305
|
+
description: "The value-free structural summary, present on a call that produced data. `format` names the shape of the rest, which is that format's own counts and structural type codes (a message type, a resource type, a transaction-set id): classification, never a value.",
|
|
1306
|
+
properties: {
|
|
1307
|
+
format: { type: "string", description: "The format the summary describes." }
|
|
1308
|
+
},
|
|
1309
|
+
required: ["format"],
|
|
1310
|
+
additionalProperties: true
|
|
1311
|
+
})
|
|
1034
1312
|
},
|
|
1035
1313
|
{
|
|
1036
1314
|
name: "convert",
|
|
1315
|
+
title: "Convert HL7 v2 to a FHIR R4 Bundle",
|
|
1037
1316
|
description: "Convert an HL7 v2 message to a FHIR R4 Bundle via @cosyte/transform. Returns the converted Bundle; an error-severity conversion issue is reported with ok=false.",
|
|
1038
1317
|
inputSchema: {
|
|
1039
1318
|
type: "object",
|
|
@@ -1047,7 +1326,21 @@ var TOOL_DEFS = [
|
|
|
1047
1326
|
},
|
|
1048
1327
|
required: ["content"],
|
|
1049
1328
|
additionalProperties: false
|
|
1050
|
-
}
|
|
1329
|
+
},
|
|
1330
|
+
outputSchema: outputSchema({
|
|
1331
|
+
type: "object",
|
|
1332
|
+
description: "The conversion payload, present on a call that produced data. An error-severity finding is a negative verdict (the Bundle is still returned), not a failed call.",
|
|
1333
|
+
properties: {
|
|
1334
|
+
format: { type: "string", description: "The conversion target the Bundle is in." },
|
|
1335
|
+
bundle: { type: "object", description: "The converted FHIR R4 Bundle: the data channel." },
|
|
1336
|
+
findings: {
|
|
1337
|
+
type: "array",
|
|
1338
|
+
description: "Value-free conversion findings: a code, a severity, and a positional locator."
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
required: ["format", "bundle", "findings"],
|
|
1342
|
+
additionalProperties: false
|
|
1343
|
+
})
|
|
1051
1344
|
}
|
|
1052
1345
|
];
|
|
1053
1346
|
function inlineDeps(content) {
|
|
@@ -1068,37 +1361,65 @@ function stringArg(args, key) {
|
|
|
1068
1361
|
const v = args[key];
|
|
1069
1362
|
return typeof v === "string" ? v : null;
|
|
1070
1363
|
}
|
|
1071
|
-
function
|
|
1072
|
-
const hardError = result.stdout === "";
|
|
1073
|
-
const text = hardError ? result.stderr.trim() : result.stdout.trim();
|
|
1364
|
+
function structuredResult(structuredContent) {
|
|
1074
1365
|
return {
|
|
1075
|
-
content: [
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
isError: hardError,
|
|
1079
|
-
structuredContent: { exit: result.exit, ok: !hardError }
|
|
1366
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
1367
|
+
isError: structuredContent.status === "failed",
|
|
1368
|
+
structuredContent
|
|
1080
1369
|
};
|
|
1081
1370
|
}
|
|
1082
|
-
function
|
|
1083
|
-
return
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1371
|
+
function isRecordLine(value) {
|
|
1372
|
+
return typeof value === "object" && value !== null && "record" in value;
|
|
1373
|
+
}
|
|
1374
|
+
function parsePayload(stdout) {
|
|
1375
|
+
const values = [];
|
|
1376
|
+
for (const line of stdout.split("\n")) {
|
|
1377
|
+
if (line.trim().length === 0) continue;
|
|
1378
|
+
try {
|
|
1379
|
+
values.push(JSON.parse(line));
|
|
1380
|
+
} catch {
|
|
1381
|
+
return void 0;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const only = values.length === 1 ? values[0] : void 0;
|
|
1385
|
+
return only !== void 0 && !isRecordLine(only) ? only : { records: values };
|
|
1386
|
+
}
|
|
1387
|
+
function diagnosticCode(stderr) {
|
|
1388
|
+
return Object.values(CLI_CODES).find((code) => stderr.includes(`cosyte: ${code}: `));
|
|
1389
|
+
}
|
|
1390
|
+
function toToolResult(result) {
|
|
1391
|
+
if (result.stdout === "") {
|
|
1392
|
+
const code = diagnosticCode(result.stderr);
|
|
1393
|
+
return structuredResult({
|
|
1394
|
+
ok: false,
|
|
1395
|
+
status: "failed",
|
|
1396
|
+
exit: result.exit,
|
|
1397
|
+
...code !== void 0 ? { code } : {}
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
const data = parsePayload(result.stdout);
|
|
1401
|
+
return structuredResult({
|
|
1402
|
+
ok: true,
|
|
1403
|
+
status: result.exit === EXIT.OK ? "success" : "verdict",
|
|
1404
|
+
exit: result.exit,
|
|
1405
|
+
...data !== void 0 ? { data } : {}
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
function usageError() {
|
|
1409
|
+
return structuredResult({
|
|
1410
|
+
ok: false,
|
|
1411
|
+
status: "failed",
|
|
1412
|
+
exit: EXIT.USAGE,
|
|
1413
|
+
code: CLI_CODES.CLI_USAGE
|
|
1414
|
+
});
|
|
1088
1415
|
}
|
|
1089
1416
|
function internalError(e) {
|
|
1090
1417
|
const err = toCliError(e);
|
|
1091
|
-
return {
|
|
1092
|
-
content: [{ type: "text", text: `cosyte: ${err.code}: ${err.message}` }],
|
|
1093
|
-
isError: true,
|
|
1094
|
-
structuredContent: { exit: err.exit, ok: false }
|
|
1095
|
-
};
|
|
1418
|
+
return structuredResult({ ok: false, status: "failed", exit: err.exit, code: err.code });
|
|
1096
1419
|
}
|
|
1097
1420
|
async function dispatchTool(name, args, deps) {
|
|
1098
1421
|
const content = stringArg(args, "content");
|
|
1099
|
-
if (content === null)
|
|
1100
|
-
return usageError("missing required 'content' argument (the message text to operate on)");
|
|
1101
|
-
}
|
|
1422
|
+
if (content === null) return usageError();
|
|
1102
1423
|
const runDeps = inlineDeps(content);
|
|
1103
1424
|
const format = stringArg(args, "format");
|
|
1104
1425
|
const fmtFlag = format !== null ? ["--format", format] : [];
|
|
@@ -1117,7 +1438,7 @@ async function dispatchTool(name, args, deps) {
|
|
|
1117
1438
|
return toToolResult(await convertCommand(["-", "--to", to, "--json"], runDeps, VALUE_FREE));
|
|
1118
1439
|
}
|
|
1119
1440
|
default:
|
|
1120
|
-
return usageError(
|
|
1441
|
+
return usageError();
|
|
1121
1442
|
}
|
|
1122
1443
|
} catch (e) {
|
|
1123
1444
|
return internalError(e);
|
|
@@ -1131,12 +1452,21 @@ function createMcpServer() {
|
|
|
1131
1452
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1132
1453
|
tools: TOOL_DEFS.map((t) => ({
|
|
1133
1454
|
name: t.name,
|
|
1455
|
+
title: t.title,
|
|
1134
1456
|
description: t.description,
|
|
1135
1457
|
inputSchema: {
|
|
1136
1458
|
type: t.inputSchema.type,
|
|
1137
1459
|
properties: { ...t.inputSchema.properties },
|
|
1138
1460
|
...t.inputSchema.required !== void 0 ? { required: [...t.inputSchema.required] } : {},
|
|
1139
1461
|
...t.inputSchema.additionalProperties !== void 0 ? { additionalProperties: t.inputSchema.additionalProperties } : {}
|
|
1462
|
+
},
|
|
1463
|
+
// Every advertised tool publishes its result schema; a client validates a reply against it
|
|
1464
|
+
// instead of pattern-matching the text block. Declared, so it is copied unconditionally.
|
|
1465
|
+
outputSchema: {
|
|
1466
|
+
type: t.outputSchema.type,
|
|
1467
|
+
properties: { ...t.outputSchema.properties },
|
|
1468
|
+
required: [...t.outputSchema.required],
|
|
1469
|
+
additionalProperties: t.outputSchema.additionalProperties
|
|
1140
1470
|
}
|
|
1141
1471
|
}))
|
|
1142
1472
|
}));
|
|
@@ -1146,7 +1476,9 @@ function createMcpServer() {
|
|
|
1146
1476
|
return {
|
|
1147
1477
|
content: result.content.map((c) => ({ type: c.type, text: c.text })),
|
|
1148
1478
|
isError: result.isError,
|
|
1149
|
-
|
|
1479
|
+
// The whole structured value reaches the wire. A field-by-field copy here would silently
|
|
1480
|
+
// truncate the payload the called tool's own schema promises.
|
|
1481
|
+
structuredContent: { ...result.structuredContent }
|
|
1150
1482
|
};
|
|
1151
1483
|
});
|
|
1152
1484
|
return server;
|
|
@@ -1157,8 +1489,31 @@ async function startStdioServer() {
|
|
|
1157
1489
|
}
|
|
1158
1490
|
|
|
1159
1491
|
// src/bin/cosyte-mcp.ts
|
|
1492
|
+
var stderrOpen = true;
|
|
1493
|
+
var reported = false;
|
|
1494
|
+
process.stderr.on("error", () => {
|
|
1495
|
+
stderrOpen = false;
|
|
1496
|
+
});
|
|
1497
|
+
function note(line) {
|
|
1498
|
+
if (!stderrOpen) return;
|
|
1499
|
+
try {
|
|
1500
|
+
process.stderr.write(line);
|
|
1501
|
+
} catch {
|
|
1502
|
+
stderrOpen = false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
process.stdout.on("error", () => {
|
|
1506
|
+
if (reported) return;
|
|
1507
|
+
reported = true;
|
|
1508
|
+
note(
|
|
1509
|
+
`cosyte-mcp: ${CLI_CODES.CLI_OUTPUT_WRITE_FAILED}: could not write to the output stream; it closed before the output was delivered
|
|
1510
|
+
`
|
|
1511
|
+
);
|
|
1512
|
+
process.exitCode = EXIT.IOERR;
|
|
1513
|
+
process.stdin.destroy();
|
|
1514
|
+
});
|
|
1160
1515
|
startStdioServer().catch(() => {
|
|
1161
|
-
|
|
1516
|
+
note("cosyte-mcp: CLI_INTERNAL: the MCP server failed to start\n");
|
|
1162
1517
|
process.exitCode = 70;
|
|
1163
1518
|
});
|
|
1164
1519
|
//# sourceMappingURL=cosyte-mcp.mjs.map
|