@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.
Files changed (50) hide show
  1. package/dist/commands/pod/helpers.d.ts +1 -1
  2. package/dist/commands/pod/helpers.d.ts.map +1 -1
  3. package/dist/commands/pod/helpers.js +5 -20
  4. package/dist/commands/pod/helpers.js.map +1 -1
  5. package/package.json +17 -5
  6. package/.dockerignore +0 -7
  7. package/.eslintrc.json +0 -23
  8. package/.prettierrc +0 -7
  9. package/Dockerfile +0 -18
  10. package/src/commands/capabilities.ts +0 -235
  11. package/src/commands/conformance.ts +0 -447
  12. package/src/commands/convert.ts +0 -164
  13. package/src/commands/pod/export.ts +0 -85
  14. package/src/commands/pod/helpers.ts +0 -449
  15. package/src/commands/pod/index.ts +0 -32
  16. package/src/commands/pod/info.ts +0 -239
  17. package/src/commands/pod/init.ts +0 -273
  18. package/src/commands/pod/query.ts +0 -224
  19. package/src/commands/serve.ts +0 -92
  20. package/src/commands/validate.ts +0 -303
  21. package/src/index.ts +0 -58
  22. package/src/lib/fhir-converter/cascade-to-fhir.ts +0 -369
  23. package/src/lib/fhir-converter/converters-clinical.ts +0 -446
  24. package/src/lib/fhir-converter/converters-demographics.ts +0 -270
  25. package/src/lib/fhir-converter/fhir-to-cascade.ts +0 -82
  26. package/src/lib/fhir-converter/index.ts +0 -215
  27. package/src/lib/fhir-converter/types.ts +0 -318
  28. package/src/lib/mcp/audit.ts +0 -107
  29. package/src/lib/mcp/server.ts +0 -192
  30. package/src/lib/mcp/tools.ts +0 -668
  31. package/src/lib/output.ts +0 -76
  32. package/src/lib/shacl-validator.ts +0 -314
  33. package/src/lib/turtle-parser.ts +0 -277
  34. package/src/shapes/checkup.shapes.ttl +0 -1459
  35. package/src/shapes/clinical.shapes.ttl +0 -1350
  36. package/src/shapes/clinical.ttl +0 -1369
  37. package/src/shapes/core.shapes.ttl +0 -450
  38. package/src/shapes/core.ttl +0 -603
  39. package/src/shapes/coverage.shapes.ttl +0 -214
  40. package/src/shapes/coverage.ttl +0 -182
  41. package/src/shapes/health.shapes.ttl +0 -697
  42. package/src/shapes/health.ttl +0 -859
  43. package/src/shapes/pots.shapes.ttl +0 -481
  44. package/test-fixtures/fhir-bundle-example.json +0 -216
  45. package/test-fixtures/fhir-medication-example.json +0 -18
  46. package/tests/cli.test.ts +0 -126
  47. package/tests/fhir-converter.test.ts +0 -874
  48. package/tests/mcp-server.test.ts +0 -396
  49. package/tests/pod.test.ts +0 -400
  50. 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
- }
@@ -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
- }
@@ -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
- }