catence 0.2.1-beta.21 → 0.2.1-beta.23
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.23";
|
|
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.23';
|
|
7
7
|
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export function catenceRuntimeHealth() {
|
|
9
9
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { SYNC_STAGES } from '../../contracts/progress.js';
|
|
3
|
+
import { STALE_RUN_TIMEOUT_MS, SYNC_STAGES } from '../../contracts/progress.js';
|
|
4
4
|
import { mergeProgress, readRunningProgress } from '../../elt/storage/progress-sidecar.js';
|
|
5
5
|
import { sqlString } from './sql.js';
|
|
6
6
|
import { DATASET_CATALOG, getDataset } from './catalog.js';
|
|
@@ -142,6 +142,11 @@ export class ReadOnlyRepository {
|
|
|
142
142
|
async progress() {
|
|
143
143
|
// Same sequential-read discipline as status(): the shared native
|
|
144
144
|
// connection must not run these small reads concurrently.
|
|
145
|
+
// A run killed with its container keeps status='running' in the database;
|
|
146
|
+
// the heartbeat age filter mirrors the sidecar contract and db.syncProgress
|
|
147
|
+
// so such a run stops reading as active (and stops blocking new syncs)
|
|
148
|
+
// after the stale-run window.
|
|
149
|
+
const cutoff = new Date(Date.now() - STALE_RUN_TIMEOUT_MS).toISOString();
|
|
145
150
|
const running = await this.rows(`
|
|
146
151
|
SELECT runs.run_id, runs.provider, coalesce(progress.stage, 'starting') AS stage,
|
|
147
152
|
progress.current_step,
|
|
@@ -154,8 +159,9 @@ export class ReadOnlyRepository {
|
|
|
154
159
|
FROM sync_runs AS runs
|
|
155
160
|
LEFT JOIN sync_run_progress AS progress USING (run_id)
|
|
156
161
|
WHERE runs.status = 'running'
|
|
162
|
+
AND coalesce(progress.heartbeat_at, runs.started_at) > $cutoff
|
|
157
163
|
ORDER BY runs.started_at DESC
|
|
158
|
-
|
|
164
|
+
`, { cutoff });
|
|
159
165
|
const recent = await this.rows(`
|
|
160
166
|
SELECT run_id, provider, stage, current_step, completed_units, total_units, percent,
|
|
161
167
|
elapsed_seconds, estimated_remaining_seconds,
|
|
@@ -22,6 +22,41 @@ function bearerTokenFrom(request) {
|
|
|
22
22
|
return null;
|
|
23
23
|
return header.slice('Bearer '.length);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* One-line diagnostics for the auth and MCP surfaces: gate outcomes and every
|
|
27
|
+
* /mcp or /oauth exchange are otherwise invisible in container logs. Routine
|
|
28
|
+
* dashboard polling stays quiet; set CATENCE_HTTP_ACCESS_LOG=0 to disable.
|
|
29
|
+
* Logs method, path, status, latency, user agent, and the MCP protocol hints —
|
|
30
|
+
* never headers carrying credentials.
|
|
31
|
+
*/
|
|
32
|
+
function logRequestCompletion(request, response, pathname) {
|
|
33
|
+
if (process.env.CATENCE_HTTP_ACCESS_LOG === '0')
|
|
34
|
+
return;
|
|
35
|
+
const startedAt = Date.now();
|
|
36
|
+
response.once('finish', () => {
|
|
37
|
+
const status = response.statusCode;
|
|
38
|
+
const interesting = pathname === '/mcp' ||
|
|
39
|
+
pathname === '/' ||
|
|
40
|
+
pathname.startsWith('/oauth/') ||
|
|
41
|
+
pathname.startsWith('/.well-known/oauth-') ||
|
|
42
|
+
status >= 400;
|
|
43
|
+
if (!interesting)
|
|
44
|
+
return;
|
|
45
|
+
const parts = [`[http] ${new Date().toISOString()}`, `${request.method} ${pathname}`, String(status), `${Date.now() - startedAt}ms`];
|
|
46
|
+
const userAgent = request.headers['user-agent'];
|
|
47
|
+
if (typeof userAgent === 'string' && userAgent)
|
|
48
|
+
parts.push(`ua="${userAgent.slice(0, 120)}"`);
|
|
49
|
+
const protocolVersion = request.headers['mcp-protocol-version'];
|
|
50
|
+
if (typeof protocolVersion === 'string' && protocolVersion)
|
|
51
|
+
parts.push(`mcp-protocol-version=${protocolVersion}`);
|
|
52
|
+
const mcpMethod = request.headers['mcp-method'];
|
|
53
|
+
if (typeof mcpMethod === 'string' && mcpMethod)
|
|
54
|
+
parts.push(`mcp-method=${mcpMethod}`);
|
|
55
|
+
if (pathname === '/mcp' || pathname === '/')
|
|
56
|
+
parts.push(`auth=${bearerTokenFrom(request) !== null ? 'bearer' : 'none'}`);
|
|
57
|
+
console.log(parts.join(' '));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
25
60
|
function matchesLegacyToken(rawToken, expected) {
|
|
26
61
|
const received = Buffer.from(rawToken);
|
|
27
62
|
const expectedBuffer = Buffer.from(expected);
|
|
@@ -438,16 +473,66 @@ function singleJsonRpcRequest(body) {
|
|
|
438
473
|
function mcpServerCapabilities(server) {
|
|
439
474
|
return server.server.getCapabilities();
|
|
440
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Answer server/discover from a throwaway scoped server so the descriptor
|
|
478
|
+
* (capabilities, server identity, instructions) can never drift from what the
|
|
479
|
+
* initialize response reports. See MODERN_MCP_PROTOCOL_VERSION.
|
|
480
|
+
*/
|
|
481
|
+
async function writeDiscoveryResult(response, paths, scope, id) {
|
|
482
|
+
const server = createCatenceMcpServer(paths, {}, { scope });
|
|
483
|
+
try {
|
|
484
|
+
json(response, 200, {
|
|
485
|
+
jsonrpc: '2.0',
|
|
486
|
+
id: id ?? null,
|
|
487
|
+
result: {
|
|
488
|
+
resultType: 'complete',
|
|
489
|
+
supportedVersions: [MODERN_MCP_PROTOCOL_VERSION, ...SUPPORTED_PROTOCOL_VERSIONS],
|
|
490
|
+
capabilities: mcpServerCapabilities(server),
|
|
491
|
+
_meta: { 'io.modelcontextprotocol/serverInfo': CATENCE_MCP_SERVER_INFO },
|
|
492
|
+
instructions: MCP_INSTRUCTIONS,
|
|
493
|
+
ttlMs: 3_600_000,
|
|
494
|
+
cacheScope: 'public',
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
await server.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Hosted connectors may probe server/discover before attaching the bearer
|
|
504
|
+
* token; the result exposes nothing athlete-scoped, so it is answered ahead
|
|
505
|
+
* of the 401. Every other unauthenticated /mcp request stays behind the gate.
|
|
506
|
+
* Returns true when the response was written.
|
|
507
|
+
*/
|
|
508
|
+
async function handleUnauthenticatedDiscovery(request, response, paths) {
|
|
509
|
+
let body;
|
|
510
|
+
try {
|
|
511
|
+
body = await readJsonBody(request);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
const jsonRpc = singleJsonRpcRequest(body);
|
|
517
|
+
if (jsonRpc?.method !== 'server/discover')
|
|
518
|
+
return false;
|
|
519
|
+
await writeDiscoveryResult(response, paths, { athleteIds: 'all' }, jsonRpc.id);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
441
522
|
async function handleMcpRequest(request, response, paths, principal) {
|
|
442
523
|
if (request.method !== 'POST') {
|
|
443
524
|
methodNotAllowed(response);
|
|
444
525
|
return;
|
|
445
526
|
}
|
|
446
|
-
// ChatGPT's discovery probe and the requests that follow it carry
|
|
447
|
-
// 2026-07-28
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
527
|
+
// ChatGPT's discovery probe and the requests that follow it carry a dated
|
|
528
|
+
// 2026 MCP revision (2026-07-28 as of today, later revisions as they ship);
|
|
529
|
+
// the SDK transport understands only its own older list, and the surface
|
|
530
|
+
// here is stateless either way, so any dated 2026 revision is bridged down
|
|
531
|
+
// while still-unknown values keep failing closed. The Node adapter rebuilds
|
|
532
|
+
// the web-standard headers from rawHeaders (not the parsed headers object),
|
|
533
|
+
// so both views are rewritten.
|
|
534
|
+
const requestedVersion = request.headers['mcp-protocol-version'];
|
|
535
|
+
if (typeof requestedVersion === 'string' && /^2026-\d{2}-\d{2}$/.test(requestedVersion)) {
|
|
451
536
|
request.headers['mcp-protocol-version'] = LATEST_PROTOCOL_VERSION;
|
|
452
537
|
const rawHeaders = request.rawHeaders;
|
|
453
538
|
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
@@ -471,25 +556,7 @@ async function handleMcpRequest(request, response, paths, principal) {
|
|
|
471
556
|
if (jsonRpc?.method === 'server/discover') {
|
|
472
557
|
// Modern clients probe discovery before anything else; answer from the
|
|
473
558
|
// same descriptor the initialize response reports so the two never drift.
|
|
474
|
-
|
|
475
|
-
try {
|
|
476
|
-
json(response, 200, {
|
|
477
|
-
jsonrpc: '2.0',
|
|
478
|
-
id: jsonRpc.id ?? null,
|
|
479
|
-
result: {
|
|
480
|
-
resultType: 'complete',
|
|
481
|
-
supportedVersions: [MODERN_MCP_PROTOCOL_VERSION, ...SUPPORTED_PROTOCOL_VERSIONS],
|
|
482
|
-
capabilities: mcpServerCapabilities(server),
|
|
483
|
-
_meta: { 'io.modelcontextprotocol/serverInfo': CATENCE_MCP_SERVER_INFO },
|
|
484
|
-
instructions: MCP_INSTRUCTIONS,
|
|
485
|
-
ttlMs: 3_600_000,
|
|
486
|
-
cacheScope: 'public',
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
finally {
|
|
491
|
-
await server.close();
|
|
492
|
-
}
|
|
559
|
+
await writeDiscoveryResult(response, paths, { athleteIds: principal.athleteIds }, jsonRpc.id);
|
|
493
560
|
return;
|
|
494
561
|
}
|
|
495
562
|
// The HTTP gate has already authenticated this request; the MCP server
|
|
@@ -599,6 +666,7 @@ export function createCatenceHttpServer(options = {}) {
|
|
|
599
666
|
return createServer((request, response) => {
|
|
600
667
|
void (async () => {
|
|
601
668
|
const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
669
|
+
logRequestCompletion(request, response, pathname);
|
|
602
670
|
// The OAuth endpoints are the auth surface itself: they bypass the gate
|
|
603
671
|
// and CORS entirely and are delegated to the Express sub-app. When OAuth
|
|
604
672
|
// is effectively disabled they answer 404 — no metadata is advertised.
|
|
@@ -626,6 +694,14 @@ export function createCatenceHttpServer(options = {}) {
|
|
|
626
694
|
? FULL_ACCESS_PRINCIPAL
|
|
627
695
|
: await resolvePrincipal(request, effectiveOptions, accountPaths);
|
|
628
696
|
if (principal === null) {
|
|
697
|
+
// A hosted connector may probe server/discover before attaching the
|
|
698
|
+
// bearer token. The discovery result exposes nothing athlete-scoped,
|
|
699
|
+
// so that one probe is answered ahead of the 401; every other
|
|
700
|
+
// unauthenticated MCP request stays behind the gate. The root alias
|
|
701
|
+
// mirrors /mcp here too so bare-domain connectors see the same gate.
|
|
702
|
+
if ((pathname === '/mcp' || pathname === '/') && request.method === 'POST' && await handleUnauthenticatedDiscovery(request, response, catalogPaths ?? staticPaths ?? resolveCatalogPaths())) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
629
705
|
// A presented-but-rejected token is a bearer problem, not an OAuth
|
|
630
706
|
// discovery problem: answer RFC 6750 invalid_token without
|
|
631
707
|
// resource_metadata, so static-key clients get a clear "your token is
|
|
@@ -1159,7 +1235,11 @@ export function createCatenceHttpServer(options = {}) {
|
|
|
1159
1235
|
}
|
|
1160
1236
|
return;
|
|
1161
1237
|
}
|
|
1162
|
-
|
|
1238
|
+
// Hosted connectors probe the URL exactly as the operator typed it, and
|
|
1239
|
+
// a bare-domain connector has no /mcp path to append. Root requests are
|
|
1240
|
+
// served exactly like /mcp; the protected-resource metadata still
|
|
1241
|
+
// advertises /mcp, so issued tokens keep their /mcp audience.
|
|
1242
|
+
if (pathname === '/mcp' || pathname === '/') {
|
|
1163
1243
|
await handleMcpRequest(request, response, catalogPaths ?? staticPaths ?? resolveCatalogPaths(), principal);
|
|
1164
1244
|
return;
|
|
1165
1245
|
}
|
package/package.json
CHANGED