@the-cascade-protocol/cli 0.2.0 → 0.2.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/dist/commands/pod/helpers.d.ts +1 -1
- package/dist/commands/pod/helpers.d.ts.map +1 -1
- package/dist/commands/pod/helpers.js +5 -20
- package/dist/commands/pod/helpers.js.map +1 -1
- package/package.json +17 -5
- package/.dockerignore +0 -7
- package/.eslintrc.json +0 -23
- package/.prettierrc +0 -7
- package/Dockerfile +0 -18
- package/src/commands/capabilities.ts +0 -235
- package/src/commands/conformance.ts +0 -447
- package/src/commands/convert.ts +0 -164
- package/src/commands/pod/export.ts +0 -85
- package/src/commands/pod/helpers.ts +0 -449
- package/src/commands/pod/index.ts +0 -32
- package/src/commands/pod/info.ts +0 -239
- package/src/commands/pod/init.ts +0 -273
- package/src/commands/pod/query.ts +0 -224
- package/src/commands/serve.ts +0 -92
- package/src/commands/validate.ts +0 -303
- package/src/index.ts +0 -58
- package/src/lib/fhir-converter/cascade-to-fhir.ts +0 -369
- package/src/lib/fhir-converter/converters-clinical.ts +0 -446
- package/src/lib/fhir-converter/converters-demographics.ts +0 -270
- package/src/lib/fhir-converter/fhir-to-cascade.ts +0 -82
- package/src/lib/fhir-converter/index.ts +0 -215
- package/src/lib/fhir-converter/types.ts +0 -318
- package/src/lib/mcp/audit.ts +0 -107
- package/src/lib/mcp/server.ts +0 -192
- package/src/lib/mcp/tools.ts +0 -668
- package/src/lib/output.ts +0 -76
- package/src/lib/shacl-validator.ts +0 -314
- package/src/lib/turtle-parser.ts +0 -277
- package/src/shapes/checkup.shapes.ttl +0 -1459
- package/src/shapes/clinical.shapes.ttl +0 -1350
- package/src/shapes/clinical.ttl +0 -1369
- package/src/shapes/core.shapes.ttl +0 -450
- package/src/shapes/core.ttl +0 -603
- package/src/shapes/coverage.shapes.ttl +0 -214
- package/src/shapes/coverage.ttl +0 -182
- package/src/shapes/health.shapes.ttl +0 -697
- package/src/shapes/health.ttl +0 -859
- package/src/shapes/pots.shapes.ttl +0 -481
- package/test-fixtures/fhir-bundle-example.json +0 -216
- package/test-fixtures/fhir-medication-example.json +0 -18
- package/tests/cli.test.ts +0 -126
- package/tests/fhir-converter.test.ts +0 -874
- package/tests/mcp-server.test.ts +0 -396
- package/tests/pod.test.ts +0 -400
- package/tsconfig.json +0 -24
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types and namespace constants for FHIR conversion.
|
|
3
|
-
*
|
|
4
|
-
* Used by both fhir-to-cascade and cascade-to-fhir converters.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { DataFactory, Writer, type Quad } from 'n3';
|
|
8
|
-
|
|
9
|
-
const { namedNode, literal, quad: makeQuad } = DataFactory;
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Types
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
export type InputFormat = 'fhir' | 'cascade' | 'c-cda';
|
|
16
|
-
export type OutputFormat = 'turtle' | 'jsonld' | 'fhir' | 'cascade';
|
|
17
|
-
|
|
18
|
-
export interface ConversionResult {
|
|
19
|
-
turtle: string;
|
|
20
|
-
jsonld?: object;
|
|
21
|
-
warnings: string[];
|
|
22
|
-
resourceType: string;
|
|
23
|
-
cascadeType: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface BatchConversionResult {
|
|
27
|
-
success: boolean;
|
|
28
|
-
output: string;
|
|
29
|
-
format: OutputFormat;
|
|
30
|
-
resourceCount: number;
|
|
31
|
-
warnings: string[];
|
|
32
|
-
errors: string[];
|
|
33
|
-
results: ConversionResult[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Namespace constants
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
export const NS = {
|
|
41
|
-
cascade: 'https://ns.cascadeprotocol.org/core/v1#',
|
|
42
|
-
health: 'https://ns.cascadeprotocol.org/health/v1#',
|
|
43
|
-
clinical: 'https://ns.cascadeprotocol.org/clinical/v1#',
|
|
44
|
-
coverage: 'https://ns.cascadeprotocol.org/coverage/v1#',
|
|
45
|
-
fhir: 'http://hl7.org/fhir/',
|
|
46
|
-
sct: 'http://snomed.info/sct/',
|
|
47
|
-
loinc: 'http://loinc.org/rdf#',
|
|
48
|
-
rxnorm: 'http://www.nlm.nih.gov/research/umls/rxnorm/',
|
|
49
|
-
icd10: 'http://hl7.org/fhir/sid/icd-10-cm/',
|
|
50
|
-
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
51
|
-
prov: 'http://www.w3.org/ns/prov#',
|
|
52
|
-
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
53
|
-
} as const;
|
|
54
|
-
|
|
55
|
-
/** Standard Turtle prefix block for all generated output. */
|
|
56
|
-
export const TURTLE_PREFIXES: Record<string, string> = {
|
|
57
|
-
cascade: NS.cascade,
|
|
58
|
-
health: NS.health,
|
|
59
|
-
clinical: NS.clinical,
|
|
60
|
-
coverage: NS.coverage,
|
|
61
|
-
fhir: NS.fhir,
|
|
62
|
-
sct: NS.sct,
|
|
63
|
-
loinc: NS.loinc,
|
|
64
|
-
rxnorm: NS.rxnorm,
|
|
65
|
-
xsd: NS.xsd,
|
|
66
|
-
prov: NS.prov,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// FHIR coding-system to Cascade namespace mapping
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
export const CODING_SYSTEM_MAP: Record<string, string> = {
|
|
74
|
-
'http://www.nlm.nih.gov/research/umls/rxnorm': NS.rxnorm,
|
|
75
|
-
'urn:oid:2.16.840.1.113883.6.88': NS.rxnorm,
|
|
76
|
-
'http://snomed.info/sct': NS.sct,
|
|
77
|
-
'http://loinc.org': NS.loinc,
|
|
78
|
-
'http://hl7.org/fhir/sid/icd-10-cm': NS.icd10,
|
|
79
|
-
'http://hl7.org/fhir/sid/icd-10': NS.icd10,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
// FHIR vital-sign LOINC code mapping
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
export const VITAL_LOINC_CODES: Record<string, { type: string; name: string; unit: string; snomedCode: string }> = {
|
|
87
|
-
'8480-6': { type: 'bloodPressureSystolic', name: 'Systolic Blood Pressure', unit: 'mmHg', snomedCode: '271649006' },
|
|
88
|
-
'8462-4': { type: 'bloodPressureDiastolic', name: 'Diastolic Blood Pressure', unit: 'mmHg', snomedCode: '271650006' },
|
|
89
|
-
'8867-4': { type: 'heartRate', name: 'Heart Rate', unit: 'bpm', snomedCode: '364075005' },
|
|
90
|
-
'9279-1': { type: 'respiratoryRate', name: 'Respiratory Rate', unit: 'breaths/min', snomedCode: '86290005' },
|
|
91
|
-
'8310-5': { type: 'bodyTemperature', name: 'Body Temperature', unit: 'degC', snomedCode: '386725007' },
|
|
92
|
-
'2708-6': { type: 'oxygenSaturation', name: 'Oxygen Saturation', unit: '%', snomedCode: '431314004' },
|
|
93
|
-
'29463-7': { type: 'bodyWeight', name: 'Body Weight', unit: 'kg', snomedCode: '27113001' },
|
|
94
|
-
'8302-2': { type: 'bodyHeight', name: 'Body Height', unit: 'cm', snomedCode: '50373000' },
|
|
95
|
-
'39156-5': { type: 'bmi', name: 'Body Mass Index', unit: 'kg/m2', snomedCode: '60621009' },
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/** FHIR observation categories that indicate vital signs */
|
|
99
|
-
export const VITAL_CATEGORIES = ['vital-signs', 'vital-sign'];
|
|
100
|
-
|
|
101
|
-
/** Set of FHIR resource types supported for conversion */
|
|
102
|
-
export const SUPPORTED_TYPES = new Set([
|
|
103
|
-
'MedicationStatement', 'MedicationRequest',
|
|
104
|
-
'Condition',
|
|
105
|
-
'AllergyIntolerance',
|
|
106
|
-
'Observation',
|
|
107
|
-
'Patient',
|
|
108
|
-
'Immunization',
|
|
109
|
-
'Coverage',
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Helper: date formatting
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Ensure an ISO 8601 dateTime string with timezone.
|
|
118
|
-
* Bare dates (YYYY-MM-DD) get T00:00:00Z appended.
|
|
119
|
-
*/
|
|
120
|
-
export function ensureDateTimeWithTz(dateStr: string): string {
|
|
121
|
-
if (!dateStr) return '';
|
|
122
|
-
// Already has time component with timezone
|
|
123
|
-
if (/T.+Z$/.test(dateStr) || /T.+[+-]\d{2}:\d{2}$/.test(dateStr)) {
|
|
124
|
-
return dateStr;
|
|
125
|
-
}
|
|
126
|
-
// Has time component but no timezone -- append Z
|
|
127
|
-
if (/T/.test(dateStr)) {
|
|
128
|
-
return dateStr + 'Z';
|
|
129
|
-
}
|
|
130
|
-
// Date only -- append midnight UTC
|
|
131
|
-
return dateStr + 'T00:00:00Z';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
// Helper: extract coding info from FHIR codeable concept
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
export interface CodingInfo {
|
|
139
|
-
system: string;
|
|
140
|
-
code: string;
|
|
141
|
-
display?: string;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function extractCodings(codeableConcept: any): CodingInfo[] {
|
|
145
|
-
if (!codeableConcept) return [];
|
|
146
|
-
const codings: CodingInfo[] = [];
|
|
147
|
-
if (Array.isArray(codeableConcept.coding)) {
|
|
148
|
-
for (const c of codeableConcept.coding) {
|
|
149
|
-
if (c.system && c.code) {
|
|
150
|
-
codings.push({ system: c.system, code: c.code, display: c.display });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return codings;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function codeableConceptText(cc: any): string | undefined {
|
|
158
|
-
if (!cc) return undefined;
|
|
159
|
-
if (cc.text) return cc.text as string;
|
|
160
|
-
if (Array.isArray(cc.coding) && cc.coding.length > 0 && cc.coding[0].display) {
|
|
161
|
-
return cc.coding[0].display as string;
|
|
162
|
-
}
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Quad-building helpers
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
export function tripleStr(subject: string, predicate: string, value: string): Quad {
|
|
171
|
-
return makeQuad(
|
|
172
|
-
namedNode(subject),
|
|
173
|
-
namedNode(predicate),
|
|
174
|
-
literal(value),
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function tripleTyped(subject: string, predicate: string, value: string, datatype: string): Quad {
|
|
179
|
-
return makeQuad(
|
|
180
|
-
namedNode(subject),
|
|
181
|
-
namedNode(predicate),
|
|
182
|
-
literal(value, namedNode(datatype)),
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function tripleBool(subject: string, predicate: string, value: boolean): Quad {
|
|
187
|
-
return makeQuad(
|
|
188
|
-
namedNode(subject),
|
|
189
|
-
namedNode(predicate),
|
|
190
|
-
literal(String(value), namedNode(NS.xsd + 'boolean')),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function tripleInt(subject: string, predicate: string, value: number): Quad {
|
|
195
|
-
return makeQuad(
|
|
196
|
-
namedNode(subject),
|
|
197
|
-
namedNode(predicate),
|
|
198
|
-
literal(String(value), namedNode(NS.xsd + 'integer')),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function tripleDouble(subject: string, predicate: string, value: number): Quad {
|
|
203
|
-
return makeQuad(
|
|
204
|
-
namedNode(subject),
|
|
205
|
-
namedNode(predicate),
|
|
206
|
-
literal(String(value), namedNode(NS.xsd + 'double')),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function tripleRef(subject: string, predicate: string, object: string): Quad {
|
|
211
|
-
return makeQuad(
|
|
212
|
-
namedNode(subject),
|
|
213
|
-
namedNode(predicate),
|
|
214
|
-
namedNode(object),
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function tripleType(subject: string, rdfType: string): Quad {
|
|
219
|
-
return tripleRef(subject, NS.rdf + 'type', rdfType);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function tripleDateTime(subject: string, predicate: string, dateStr: string): Quad {
|
|
223
|
-
return tripleTyped(subject, predicate, ensureDateTimeWithTz(dateStr), NS.xsd + 'dateTime');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function tripleDate(subject: string, predicate: string, dateStr: string): Quad {
|
|
227
|
-
return tripleTyped(subject, predicate, dateStr, NS.xsd + 'date');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Common triples every Cascade resource gets */
|
|
231
|
-
export function commonTriples(subject: string): Quad[] {
|
|
232
|
-
return [
|
|
233
|
-
tripleRef(subject, NS.cascade + 'dataProvenance', NS.cascade + 'ClinicalGenerated'),
|
|
234
|
-
tripleStr(subject, NS.cascade + 'schemaVersion', '1.3'),
|
|
235
|
-
];
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
// Quads -> Turtle serialization
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
|
|
242
|
-
export function quadsToTurtle(quads: Quad[]): Promise<string> {
|
|
243
|
-
return new Promise((resolve, reject) => {
|
|
244
|
-
const writer = new Writer({ prefixes: TURTLE_PREFIXES });
|
|
245
|
-
for (const q of quads) {
|
|
246
|
-
writer.addQuad(q);
|
|
247
|
-
}
|
|
248
|
-
writer.end((error, result) => {
|
|
249
|
-
if (error) reject(error);
|
|
250
|
-
else resolve(result);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
// Quads -> JSON-LD object (lightweight, no @context resolution)
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
export function quadsToJsonLd(quads: Quad[], _cascadeType: string): object {
|
|
260
|
-
// Build a simple JSON-LD representation grouped by subject
|
|
261
|
-
const subjects = new Map<string, Record<string, any>>();
|
|
262
|
-
|
|
263
|
-
for (const q of quads) {
|
|
264
|
-
const subj = q.subject.value;
|
|
265
|
-
if (!subjects.has(subj)) {
|
|
266
|
-
subjects.set(subj, {
|
|
267
|
-
'@context': 'https://ns.cascadeprotocol.org/context/v1/cascade.jsonld',
|
|
268
|
-
'@id': subj,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
const obj = subjects.get(subj)!;
|
|
272
|
-
const pred = q.predicate.value;
|
|
273
|
-
|
|
274
|
-
if (pred === NS.rdf + 'type') {
|
|
275
|
-
obj['@type'] = q.object.value;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Compact the predicate using known prefixes
|
|
280
|
-
let key = pred;
|
|
281
|
-
for (const [prefix, uri] of Object.entries(TURTLE_PREFIXES)) {
|
|
282
|
-
if (pred.startsWith(uri)) {
|
|
283
|
-
key = `${prefix}:${pred.slice(uri.length)}`;
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Handle object vs literal
|
|
289
|
-
if (q.object.termType === 'NamedNode') {
|
|
290
|
-
// Check if this is a provenance reference
|
|
291
|
-
let idVal = q.object.value;
|
|
292
|
-
for (const [prefix, uri] of Object.entries(TURTLE_PREFIXES)) {
|
|
293
|
-
if (idVal.startsWith(uri)) {
|
|
294
|
-
idVal = `${prefix}:${idVal.slice(uri.length)}`;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
obj[key] = { '@id': idVal };
|
|
299
|
-
} else {
|
|
300
|
-
// Literal
|
|
301
|
-
const dt = (q.object as any).datatype?.value;
|
|
302
|
-
if (dt === NS.xsd + 'dateTime' || dt === NS.xsd + 'date') {
|
|
303
|
-
obj[key] = { '@value': q.object.value, '@type': dt === NS.xsd + 'dateTime' ? 'xsd:dateTime' : 'xsd:date' };
|
|
304
|
-
} else if (dt === NS.xsd + 'boolean') {
|
|
305
|
-
obj[key] = q.object.value === 'true';
|
|
306
|
-
} else if (dt === NS.xsd + 'integer') {
|
|
307
|
-
obj[key] = parseInt(q.object.value, 10);
|
|
308
|
-
} else if (dt === NS.xsd + 'double' || dt === NS.xsd + 'decimal') {
|
|
309
|
-
obj[key] = parseFloat(q.object.value);
|
|
310
|
-
} else {
|
|
311
|
-
obj[key] = q.object.value;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const entries = Array.from(subjects.values());
|
|
317
|
-
return entries.length === 1 ? entries[0] : entries;
|
|
318
|
-
}
|
package/src/lib/mcp/audit.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Audit logging for Cascade MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Writes audit entries to a Pod's provenance/audit-log.ttl file
|
|
5
|
-
* to track agent access to health data.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from 'fs/promises';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import { randomUUID } from 'crypto';
|
|
11
|
-
|
|
12
|
-
export interface AuditEntry {
|
|
13
|
-
operation: string;
|
|
14
|
-
dataTypes: string[];
|
|
15
|
-
agentId: string;
|
|
16
|
-
recordsAccessed: number;
|
|
17
|
-
timestamp: string;
|
|
18
|
-
podPath?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Generate a globally unique audit entry ID using crypto.randomUUID().
|
|
23
|
-
* Unlike sequential counters, UUID-based IDs survive server restarts
|
|
24
|
-
* and concurrent sessions without collisions.
|
|
25
|
-
*/
|
|
26
|
-
function nextAuditId(): string {
|
|
27
|
-
return `audit-${randomUUID()}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Format an audit entry as Cascade Turtle.
|
|
32
|
-
*/
|
|
33
|
-
function formatAuditEntry(entry: AuditEntry): string {
|
|
34
|
-
const id = nextAuditId();
|
|
35
|
-
const dataTypesStr = entry.dataTypes.map((t) => `"${t}"`).join(' ');
|
|
36
|
-
|
|
37
|
-
return `
|
|
38
|
-
<#${id}> a cascade:AuditEntry ;
|
|
39
|
-
cascade:timestamp "${entry.timestamp}"^^xsd:dateTime ;
|
|
40
|
-
cascade:operation "${entry.operation}" ;
|
|
41
|
-
cascade:dataTypes (${dataTypesStr}) ;
|
|
42
|
-
cascade:agentId "${entry.agentId}" ;
|
|
43
|
-
cascade:recordsAccessed ${entry.recordsAccessed} .
|
|
44
|
-
`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Turtle prefixes for the audit log file.
|
|
49
|
-
*/
|
|
50
|
-
const AUDIT_PREFIXES = `@prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
|
|
51
|
-
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
52
|
-
@prefix prov: <http://www.w3.org/ns/prov#> .
|
|
53
|
-
|
|
54
|
-
# Cascade Protocol Agent Audit Log
|
|
55
|
-
# Generated by cascade serve --mcp
|
|
56
|
-
# This file records all agent operations on this Pod.
|
|
57
|
-
|
|
58
|
-
`;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Append an audit entry to a Pod's audit log.
|
|
62
|
-
* Creates the provenance/ directory and audit-log.ttl if they don't exist.
|
|
63
|
-
*/
|
|
64
|
-
export async function writeAuditEntry(podPath: string, entry: AuditEntry): Promise<void> {
|
|
65
|
-
const provenanceDir = path.join(podPath, 'provenance');
|
|
66
|
-
const auditLogPath = path.join(provenanceDir, 'audit-log.ttl');
|
|
67
|
-
|
|
68
|
-
// Ensure provenance directory exists
|
|
69
|
-
await fs.mkdir(provenanceDir, { recursive: true });
|
|
70
|
-
|
|
71
|
-
// Check if audit log exists
|
|
72
|
-
let exists = false;
|
|
73
|
-
try {
|
|
74
|
-
await fs.access(auditLogPath);
|
|
75
|
-
exists = true;
|
|
76
|
-
} catch {
|
|
77
|
-
// File doesn't exist yet
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const turtleEntry = formatAuditEntry(entry);
|
|
81
|
-
|
|
82
|
-
if (exists) {
|
|
83
|
-
// Append to existing file
|
|
84
|
-
await fs.appendFile(auditLogPath, turtleEntry, 'utf-8');
|
|
85
|
-
} else {
|
|
86
|
-
// Create new file with prefixes
|
|
87
|
-
await fs.writeFile(auditLogPath, AUDIT_PREFIXES + turtleEntry, 'utf-8');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Create a standard audit entry for a tool invocation.
|
|
93
|
-
*/
|
|
94
|
-
export function createAuditEntry(
|
|
95
|
-
operation: string,
|
|
96
|
-
dataTypes: string[],
|
|
97
|
-
recordsAccessed: number,
|
|
98
|
-
agentId?: string,
|
|
99
|
-
): AuditEntry {
|
|
100
|
-
return {
|
|
101
|
-
operation,
|
|
102
|
-
dataTypes,
|
|
103
|
-
agentId: agentId ?? 'unknown-agent',
|
|
104
|
-
recordsAccessed,
|
|
105
|
-
timestamp: new Date().toISOString(),
|
|
106
|
-
};
|
|
107
|
-
}
|
package/src/lib/mcp/server.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cascade Protocol MCP Server.
|
|
3
|
-
*
|
|
4
|
-
* Provides a local MCP-compatible server that exposes Cascade Protocol
|
|
5
|
-
* tools to AI agents. Supports stdio and SSE transports.
|
|
6
|
-
*
|
|
7
|
-
* All operations are local — zero network calls.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* cascade serve --mcp (stdio transport)
|
|
11
|
-
* cascade serve --mcp --transport sse --port 3000 (SSE transport)
|
|
12
|
-
*
|
|
13
|
-
* Claude Desktop configuration:
|
|
14
|
-
* {
|
|
15
|
-
* "mcpServers": {
|
|
16
|
-
* "cascade": {
|
|
17
|
-
* "command": "cascade",
|
|
18
|
-
* "args": ["serve", "--mcp"],
|
|
19
|
-
* "env": { "CASCADE_POD_PATH": "/path/to/pod" }
|
|
20
|
-
* }
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
26
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
27
|
-
import { registerTools, setDefaultPodPath } from './tools.js';
|
|
28
|
-
|
|
29
|
-
export interface ServeOptions {
|
|
30
|
-
transport: 'stdio' | 'sse';
|
|
31
|
-
port: number;
|
|
32
|
-
podPath?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create and configure the MCP server with all Cascade tools.
|
|
37
|
-
*/
|
|
38
|
-
export function createServer(): McpServer {
|
|
39
|
-
const server = new McpServer(
|
|
40
|
-
{
|
|
41
|
-
name: 'cascade-protocol',
|
|
42
|
-
version: '0.2.0',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
capabilities: {
|
|
46
|
-
tools: {},
|
|
47
|
-
},
|
|
48
|
-
instructions:
|
|
49
|
-
'Cascade Protocol MCP Server — Local-first access to structured health data. ' +
|
|
50
|
-
'Use cascade_capabilities to discover all available tools. ' +
|
|
51
|
-
'All operations are local (zero network calls). ' +
|
|
52
|
-
'All agent-written data is automatically tagged with AIGenerated provenance. ' +
|
|
53
|
-
'Set CASCADE_POD_PATH environment variable to specify the default Pod directory.',
|
|
54
|
-
},
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Register all tools
|
|
58
|
-
registerTools(server);
|
|
59
|
-
|
|
60
|
-
return server;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Start the MCP server with the specified transport.
|
|
65
|
-
*/
|
|
66
|
-
export async function startServer(options: ServeOptions): Promise<void> {
|
|
67
|
-
// Set default Pod path from env or option
|
|
68
|
-
const podPath = options.podPath ?? process.env['CASCADE_POD_PATH'];
|
|
69
|
-
if (podPath) {
|
|
70
|
-
setDefaultPodPath(podPath);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const server = createServer();
|
|
74
|
-
|
|
75
|
-
if (options.transport === 'stdio') {
|
|
76
|
-
const transport = new StdioServerTransport();
|
|
77
|
-
await server.connect(transport);
|
|
78
|
-
|
|
79
|
-
// Log to stderr so it doesn't interfere with stdio transport
|
|
80
|
-
console.error('[cascade] MCP server started (stdio transport)');
|
|
81
|
-
if (podPath) {
|
|
82
|
-
console.error(`[cascade] Default Pod path: ${podPath}`);
|
|
83
|
-
} else {
|
|
84
|
-
console.error('[cascade] No default Pod path set. Pass "path" argument to tools or set CASCADE_POD_PATH.');
|
|
85
|
-
}
|
|
86
|
-
} else if (options.transport === 'sse') {
|
|
87
|
-
// SSE transport requires an HTTP server
|
|
88
|
-
await startSSEServer(server, options.port, podPath);
|
|
89
|
-
} else {
|
|
90
|
-
throw new Error(`Unsupported transport: ${options.transport}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Start an SSE-based MCP server on the specified port.
|
|
96
|
-
*/
|
|
97
|
-
async function startSSEServer(server: McpServer, port: number, podPath?: string): Promise<void> {
|
|
98
|
-
// Dynamic import to avoid loading http module for stdio transport
|
|
99
|
-
const { SSEServerTransport } = await import('@modelcontextprotocol/sdk/server/sse.js');
|
|
100
|
-
const http = await import('http');
|
|
101
|
-
|
|
102
|
-
// Track active transports for cleanup
|
|
103
|
-
const transports = new Map<string, InstanceType<typeof SSEServerTransport>>();
|
|
104
|
-
|
|
105
|
-
const httpServer = http.createServer(async (req, res) => {
|
|
106
|
-
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
107
|
-
|
|
108
|
-
// CORS headers — restricted to localhost origins only.
|
|
109
|
-
// Requests without an Origin header (same-origin, CLI tools, curl) are allowed.
|
|
110
|
-
const origin = req.headers['origin'];
|
|
111
|
-
if (origin) {
|
|
112
|
-
try {
|
|
113
|
-
const originUrl = new URL(origin);
|
|
114
|
-
const isLocalhost = originUrl.hostname === 'localhost' || originUrl.hostname === '127.0.0.1';
|
|
115
|
-
if (!isLocalhost) {
|
|
116
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
117
|
-
res.end(JSON.stringify({ error: 'Forbidden: only localhost origins are allowed' }));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
121
|
-
} catch {
|
|
122
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
123
|
-
res.end(JSON.stringify({ error: 'Forbidden: invalid Origin header' }));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
128
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
129
|
-
|
|
130
|
-
if (req.method === 'OPTIONS') {
|
|
131
|
-
res.writeHead(204);
|
|
132
|
-
res.end();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (url.pathname === '/sse' && req.method === 'GET') {
|
|
137
|
-
// Create a new SSE transport for this connection
|
|
138
|
-
const transport = new SSEServerTransport('/messages', res);
|
|
139
|
-
const sessionId = transport.sessionId;
|
|
140
|
-
transports.set(sessionId, transport);
|
|
141
|
-
|
|
142
|
-
// Clean up on disconnect
|
|
143
|
-
res.on('close', () => {
|
|
144
|
-
transports.delete(sessionId);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
await server.connect(transport);
|
|
148
|
-
} else if (url.pathname === '/messages' && req.method === 'POST') {
|
|
149
|
-
// Find the transport by session ID from query params
|
|
150
|
-
const sessionId = url.searchParams.get('sessionId');
|
|
151
|
-
if (!sessionId || !transports.has(sessionId)) {
|
|
152
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
153
|
-
res.end(JSON.stringify({ error: 'Invalid or missing sessionId' }));
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const transport = transports.get(sessionId)!;
|
|
158
|
-
await transport.handlePostMessage(req, res);
|
|
159
|
-
} else if (url.pathname === '/health' && req.method === 'GET') {
|
|
160
|
-
// Health check endpoint
|
|
161
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
|
-
res.end(JSON.stringify({
|
|
163
|
-
status: 'ok',
|
|
164
|
-
server: 'cascade-protocol',
|
|
165
|
-
version: '0.2.0',
|
|
166
|
-
transport: 'sse',
|
|
167
|
-
activeSessions: transports.size,
|
|
168
|
-
podPath: podPath ?? 'not set',
|
|
169
|
-
}));
|
|
170
|
-
} else {
|
|
171
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
172
|
-
res.end(JSON.stringify({
|
|
173
|
-
error: 'Not found',
|
|
174
|
-
endpoints: {
|
|
175
|
-
'/sse': 'SSE connection endpoint (GET)',
|
|
176
|
-
'/messages': 'Message endpoint (POST)',
|
|
177
|
-
'/health': 'Health check (GET)',
|
|
178
|
-
},
|
|
179
|
-
}));
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
httpServer.listen(port, () => {
|
|
184
|
-
console.error(`[cascade] MCP server started (SSE transport)`);
|
|
185
|
-
console.error(`[cascade] Listening on http://localhost:${port}`);
|
|
186
|
-
console.error(`[cascade] SSE endpoint: http://localhost:${port}/sse`);
|
|
187
|
-
console.error(`[cascade] Health check: http://localhost:${port}/health`);
|
|
188
|
-
if (podPath) {
|
|
189
|
-
console.error(`[cascade] Default Pod path: ${podPath}`);
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
}
|