catence 0.2.1-beta.23 → 0.2.1-beta.24
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.
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export declare const CATENCE_RUNTIME_VERSION = "0.2.1-beta.
|
|
6
|
+
export declare const CATENCE_RUNTIME_VERSION = "0.2.1-beta.24";
|
|
7
7
|
export declare const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export type CatenceRuntimeCapabilities = {
|
|
9
9
|
mcp: true;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export const CATENCE_RUNTIME_VERSION = '0.2.1-beta.
|
|
6
|
+
export const CATENCE_RUNTIME_VERSION = '0.2.1-beta.24';
|
|
7
7
|
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export function catenceRuntimeHealth() {
|
|
9
9
|
return {
|
|
@@ -519,6 +519,141 @@ async function handleUnauthenticatedDiscovery(request, response, paths) {
|
|
|
519
519
|
await writeDiscoveryResult(response, paths, { athleteIds: 'all' }, jsonRpc.id);
|
|
520
520
|
return true;
|
|
521
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* The 2026-07-28 revision frames every completed result with `resultType` and
|
|
524
|
+
* requires caching hints on cacheable operations (server/discover, tools/list,
|
|
525
|
+
* prompts/list, resources/list, resources/templates/list, resources/read).
|
|
526
|
+
* The bundled SDK emits the legacy shape, so responses negotiated under a
|
|
527
|
+
* dated 2026 revision are rewritten on the way out. List results vary with the
|
|
528
|
+
* caller's scope, so cacheable responses from this server are always
|
|
529
|
+
* `private`. Errors and notifications carry no result and pass through.
|
|
530
|
+
*/
|
|
531
|
+
const MODERN_CACHEABLE_METHODS = new Set(['server/discover', 'tools/list', 'prompts/list', 'resources/list', 'resources/templates/list', 'resources/read']);
|
|
532
|
+
const MODERN_RESULT_TTL_MS = 300_000;
|
|
533
|
+
/** Rewrite one JSON-RPC payload; null when it carries nothing to change. */
|
|
534
|
+
function frameModernRpcPayload(payload, method) {
|
|
535
|
+
let envelope;
|
|
536
|
+
try {
|
|
537
|
+
envelope = JSON.parse(payload);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
if (typeof envelope !== 'object' || envelope === null || Array.isArray(envelope))
|
|
543
|
+
return null;
|
|
544
|
+
const record = envelope;
|
|
545
|
+
const result = record.result;
|
|
546
|
+
if (typeof result !== 'object' || result === null || Array.isArray(result))
|
|
547
|
+
return null;
|
|
548
|
+
const framed = { ...result };
|
|
549
|
+
let changed = false;
|
|
550
|
+
if (!('resultType' in framed)) {
|
|
551
|
+
framed.resultType = 'complete';
|
|
552
|
+
changed = true;
|
|
553
|
+
}
|
|
554
|
+
if (method !== undefined && MODERN_CACHEABLE_METHODS.has(method)) {
|
|
555
|
+
if (!('ttlMs' in framed)) {
|
|
556
|
+
framed.ttlMs = MODERN_RESULT_TTL_MS;
|
|
557
|
+
changed = true;
|
|
558
|
+
}
|
|
559
|
+
if (!('cacheScope' in framed)) {
|
|
560
|
+
framed.cacheScope = 'private';
|
|
561
|
+
changed = true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return changed ? JSON.stringify({ ...record, result: framed }) : null;
|
|
565
|
+
}
|
|
566
|
+
/** Rewrite a whole response body (SSE event stream or plain JSON). */
|
|
567
|
+
function frameModernResponseBody(body, contentType, method) {
|
|
568
|
+
// The adapter hands headers straight to writeHead without caching them, so
|
|
569
|
+
// the content type may be unreadable here; the body shape is a reliable
|
|
570
|
+
// fallback (SSE payloads carry `data:` lines, JSON payloads open with `{`).
|
|
571
|
+
const isEventStream = (contentType !== undefined && contentType.includes('text/event-stream')) ||
|
|
572
|
+
body.startsWith('event:') ||
|
|
573
|
+
body.startsWith('data:') ||
|
|
574
|
+
body.includes('\ndata:');
|
|
575
|
+
if (isEventStream) {
|
|
576
|
+
let changed = false;
|
|
577
|
+
const lines = body.split('\n').map((line) => {
|
|
578
|
+
if (!line.startsWith('data:'))
|
|
579
|
+
return line;
|
|
580
|
+
const framed = frameModernRpcPayload(line.slice('data:'.length).trim(), method);
|
|
581
|
+
if (framed === null)
|
|
582
|
+
return line;
|
|
583
|
+
changed = true;
|
|
584
|
+
return `data: ${framed}`;
|
|
585
|
+
});
|
|
586
|
+
return changed ? lines.join('\n') : null;
|
|
587
|
+
}
|
|
588
|
+
if ((contentType !== undefined && contentType.includes('application/json')) || body.trimStart().startsWith('{')) {
|
|
589
|
+
return frameModernRpcPayload(body, method);
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Buffer the outgoing body so the modern framing is in place before any byte
|
|
595
|
+
* reaches the socket; bodies that need no rewriting are forwarded verbatim.
|
|
596
|
+
* The Node adapter may flush headers early for streamed (chunked) responses,
|
|
597
|
+
* which stays valid because rewritten bodies only ever drop `content-length`
|
|
598
|
+
* (SSE responses never carry one to begin with).
|
|
599
|
+
*/
|
|
600
|
+
function installModernFraming(response, method) {
|
|
601
|
+
const chunks = [];
|
|
602
|
+
const append = (chunk, encoding) => {
|
|
603
|
+
if (typeof chunk === 'string')
|
|
604
|
+
chunks.push(Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8'));
|
|
605
|
+
else if (Buffer.isBuffer(chunk))
|
|
606
|
+
chunks.push(chunk);
|
|
607
|
+
else if (chunk instanceof Uint8Array)
|
|
608
|
+
chunks.push(Buffer.from(chunk));
|
|
609
|
+
else if (chunk !== undefined && chunk !== null)
|
|
610
|
+
chunks.push(Buffer.from(String(chunk)));
|
|
611
|
+
};
|
|
612
|
+
const originalEnd = response.end.bind(response);
|
|
613
|
+
const originalWriteHead = response.writeHead.bind(response);
|
|
614
|
+
// Headers handed straight to writeHead are not cached for getHeader and may
|
|
615
|
+
// reach the socket immediately, so a computed content-length is dropped
|
|
616
|
+
// here before any byte leaves the process: the framed body differs in size
|
|
617
|
+
// and chunked framing is always valid for these responses. (`removeHeader`
|
|
618
|
+
// is not an option at end time — by then the headers are already sent.)
|
|
619
|
+
const withoutLength = (value) => {
|
|
620
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
621
|
+
return value;
|
|
622
|
+
const copy = {};
|
|
623
|
+
for (const [key, headerValue] of Object.entries(value)) {
|
|
624
|
+
if (key.toLowerCase() !== 'content-length')
|
|
625
|
+
copy[key] = headerValue;
|
|
626
|
+
}
|
|
627
|
+
return copy;
|
|
628
|
+
};
|
|
629
|
+
response.writeHead = ((statusCode, arg1, arg2) => {
|
|
630
|
+
if (typeof arg1 === 'string')
|
|
631
|
+
return originalWriteHead(statusCode, arg1, withoutLength(arg2));
|
|
632
|
+
return originalWriteHead(statusCode, withoutLength(arg1));
|
|
633
|
+
});
|
|
634
|
+
response.write = ((chunk, encodingOrCallback) => {
|
|
635
|
+
append(chunk, encodingOrCallback);
|
|
636
|
+
if (typeof encodingOrCallback === 'function')
|
|
637
|
+
queueMicrotask(encodingOrCallback);
|
|
638
|
+
return true;
|
|
639
|
+
});
|
|
640
|
+
response.end = ((chunk, encodingOrCallback, callback) => {
|
|
641
|
+
if (response.writableEnded)
|
|
642
|
+
return response;
|
|
643
|
+
append(chunk, encodingOrCallback);
|
|
644
|
+
const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : typeof callback === 'function' ? callback : undefined;
|
|
645
|
+
const raw = Buffer.concat(chunks);
|
|
646
|
+
const header = response.getHeader('content-type');
|
|
647
|
+
const contentType = Array.isArray(header) ? header.join(', ') : header === undefined ? undefined : String(header);
|
|
648
|
+
const framed = frameModernResponseBody(raw.toString('utf8'), contentType, method);
|
|
649
|
+
const final = framed === null ? raw : Buffer.from(framed, 'utf8');
|
|
650
|
+
if (done)
|
|
651
|
+
originalEnd(final, done);
|
|
652
|
+
else
|
|
653
|
+
originalEnd(final);
|
|
654
|
+
return response;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
522
657
|
async function handleMcpRequest(request, response, paths, principal) {
|
|
523
658
|
if (request.method !== 'POST') {
|
|
524
659
|
methodNotAllowed(response);
|
|
@@ -532,7 +667,8 @@ async function handleMcpRequest(request, response, paths, principal) {
|
|
|
532
667
|
// the web-standard headers from rawHeaders (not the parsed headers object),
|
|
533
668
|
// so both views are rewritten.
|
|
534
669
|
const requestedVersion = request.headers['mcp-protocol-version'];
|
|
535
|
-
|
|
670
|
+
const modernRequest = typeof requestedVersion === 'string' && /^2026-\d{2}-\d{2}$/.test(requestedVersion);
|
|
671
|
+
if (modernRequest) {
|
|
536
672
|
request.headers['mcp-protocol-version'] = LATEST_PROTOCOL_VERSION;
|
|
537
673
|
const rawHeaders = request.rawHeaders;
|
|
538
674
|
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
@@ -559,6 +695,10 @@ async function handleMcpRequest(request, response, paths, principal) {
|
|
|
559
695
|
await writeDiscoveryResult(response, paths, { athleteIds: principal.athleteIds }, jsonRpc.id);
|
|
560
696
|
return;
|
|
561
697
|
}
|
|
698
|
+
// A dated 2026 revision expects the SDK's legacy result shape reframed with
|
|
699
|
+
// `resultType`/caching fields before it leaves the process.
|
|
700
|
+
if (modernRequest)
|
|
701
|
+
installModernFraming(response, jsonRpc?.method);
|
|
562
702
|
// The HTTP gate has already authenticated this request; the MCP server
|
|
563
703
|
// re-enforces the principal's athlete scope on every tool and roster.
|
|
564
704
|
const server = createCatenceMcpServer(paths, {}, { scope: { athleteIds: principal.athleteIds } });
|
package/package.json
CHANGED