@cachly-dev/mcp-server 0.10.45 → 0.10.47
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/README.md +39 -60
- package/dist/__tests__/brain-flow.test.js +0 -141
- package/dist/__tests__/brain-flow.test.js.map +1 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
- package/dist/__tests__/brain-predict-failures.test.js +339 -0
- package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
- package/dist/handlers/brain.js +21 -120
- package/dist/handlers/brain.js.map +1 -1
- package/dist/index.js +80 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/smithery.yaml +10 -21
- package/src/__tests__/brain-flow.test.ts +0 -174
- package/src/__tests__/brain-predict-failures.test.ts +430 -0
- package/src/handlers/brain.ts +22 -124
- package/src/index.ts +131 -39
package/dist/index.js
CHANGED
|
@@ -45,11 +45,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
45
45
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
46
46
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
47
47
|
import { Redis } from 'ioredis';
|
|
48
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
48
49
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
49
50
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
50
51
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
51
52
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
52
|
-
const CURRENT_VERSION = '0.10.
|
|
53
|
+
const CURRENT_VERSION = '0.10.47';
|
|
54
|
+
const reqCtx = new AsyncLocalStorage();
|
|
55
|
+
function getJWT() { return reqCtx.getStore()?.jwt || JWT; }
|
|
56
|
+
function getDefaultInstanceId() { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
|
|
53
57
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
54
58
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
55
59
|
// When neither is set, we auto-fetch the first running instance once per process.
|
|
@@ -57,9 +61,13 @@ let _defaultInstanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
|
57
61
|
// Timestamp of last failed fetch — retries after 30 s (not permanently blocked).
|
|
58
62
|
let _defaultInstanceLastAttempt = 0;
|
|
59
63
|
async function resolveDefaultInstanceId() {
|
|
64
|
+
// In an HTTP request scope the instance ID is fixed by the caller's header.
|
|
65
|
+
const scopedId = reqCtx.getStore()?.instanceId;
|
|
66
|
+
if (scopedId)
|
|
67
|
+
return scopedId;
|
|
60
68
|
if (_defaultInstanceId)
|
|
61
69
|
return _defaultInstanceId;
|
|
62
|
-
if (!
|
|
70
|
+
if (!getJWT())
|
|
63
71
|
return '';
|
|
64
72
|
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
65
73
|
const now = Date.now();
|
|
@@ -68,7 +76,7 @@ async function resolveDefaultInstanceId() {
|
|
|
68
76
|
_defaultInstanceLastAttempt = now;
|
|
69
77
|
try {
|
|
70
78
|
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
71
|
-
headers: { Authorization: `Bearer ${
|
|
79
|
+
headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
|
|
72
80
|
signal: AbortSignal.timeout(5000),
|
|
73
81
|
});
|
|
74
82
|
if (!res.ok)
|
|
@@ -154,7 +162,7 @@ async function pollDeviceFlow(flow) {
|
|
|
154
162
|
try {
|
|
155
163
|
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
156
164
|
method: 'POST',
|
|
157
|
-
headers: { Authorization: `Bearer ${
|
|
165
|
+
headers: { Authorization: `Bearer ${getJWT()}`, 'Content-Type': 'application/json' },
|
|
158
166
|
signal: AbortSignal.timeout(12000),
|
|
159
167
|
});
|
|
160
168
|
if (autoRes.ok) {
|
|
@@ -167,7 +175,7 @@ async function pollDeviceFlow(flow) {
|
|
|
167
175
|
// Log so telemetry can detect free-tier auto-provision failures
|
|
168
176
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
169
177
|
method: 'POST',
|
|
170
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${
|
|
178
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getJWT()}` },
|
|
171
179
|
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
172
180
|
signal: AbortSignal.timeout(3000),
|
|
173
181
|
}).catch(() => { });
|
|
@@ -180,7 +188,7 @@ async function pollDeviceFlow(flow) {
|
|
|
180
188
|
if (_defaultInstanceId) {
|
|
181
189
|
try {
|
|
182
190
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
183
|
-
headers: { Authorization: `Bearer ${
|
|
191
|
+
headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
|
|
184
192
|
signal: AbortSignal.timeout(4000),
|
|
185
193
|
});
|
|
186
194
|
if (checkRes.ok) {
|
|
@@ -191,7 +199,7 @@ async function pollDeviceFlow(flow) {
|
|
|
191
199
|
while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
|
|
192
200
|
await new Promise(r => setTimeout(r, 3000));
|
|
193
201
|
const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
194
|
-
headers: { Authorization: `Bearer ${
|
|
202
|
+
headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
|
|
195
203
|
signal: AbortSignal.timeout(4000),
|
|
196
204
|
}).catch(() => null);
|
|
197
205
|
if (rr?.ok) {
|
|
@@ -250,7 +258,7 @@ async function getConnection(instance_id) {
|
|
|
250
258
|
// Telemetry: unreachable instance so the team can proactively investigate
|
|
251
259
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
252
260
|
method: 'POST',
|
|
253
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
261
|
+
headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
|
|
254
262
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
255
263
|
signal: AbortSignal.timeout(3000),
|
|
256
264
|
}).catch(() => { });
|
|
@@ -319,7 +327,7 @@ async function getConnection(instance_id) {
|
|
|
319
327
|
const msg = err instanceof Error ? err.message : String(err);
|
|
320
328
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
321
329
|
method: 'POST',
|
|
322
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
330
|
+
headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
|
|
323
331
|
body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
|
|
324
332
|
signal: AbortSignal.timeout(3000),
|
|
325
333
|
}).catch(() => { });
|
|
@@ -333,14 +341,14 @@ async function getConnection(instance_id) {
|
|
|
333
341
|
}
|
|
334
342
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
335
343
|
async function apiFetch(path, options = {}) {
|
|
336
|
-
checkJwt(
|
|
344
|
+
checkJwt(getJWT());
|
|
337
345
|
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
338
346
|
const res = await fetch(`${API_URL}${path}`, {
|
|
339
347
|
...options,
|
|
340
348
|
signal: AbortSignal.timeout(timeoutMs),
|
|
341
349
|
headers: {
|
|
342
350
|
'Content-Type': 'application/json',
|
|
343
|
-
Authorization: `Bearer ${
|
|
351
|
+
Authorization: `Bearer ${getJWT()}`,
|
|
344
352
|
...options.headers,
|
|
345
353
|
},
|
|
346
354
|
});
|
|
@@ -384,7 +392,7 @@ function sendFunnelEvent(event, extra) {
|
|
|
384
392
|
headers: { 'Content-Type': 'application/json' },
|
|
385
393
|
body: JSON.stringify({
|
|
386
394
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
387
|
-
...(
|
|
395
|
+
...(getJWT() ? { jwt: getJWT() } : {}),
|
|
388
396
|
...extra,
|
|
389
397
|
}),
|
|
390
398
|
signal: AbortSignal.timeout(3000),
|
|
@@ -408,7 +416,7 @@ async function sendAnonymousTelemetry(toolName) {
|
|
|
408
416
|
}
|
|
409
417
|
async function handleTool(name, args) {
|
|
410
418
|
// Guard: if no JWT, return actionable onboarding message instead of HTTP 401
|
|
411
|
-
if (!
|
|
419
|
+
if (!getJWT()) {
|
|
412
420
|
void sendAnonymousTelemetry(name);
|
|
413
421
|
// ── Zero-credential device flow ─────────────────────────────────────
|
|
414
422
|
// 1st call: start device flow, return code + URL
|
|
@@ -485,14 +493,14 @@ async function handleTool(name, args) {
|
|
|
485
493
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
486
494
|
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
487
495
|
if (brainResult !== null) {
|
|
488
|
-
if (!_firstCallSuccessSent &&
|
|
496
|
+
if (!_firstCallSuccessSent && getJWT()) {
|
|
489
497
|
_firstCallSuccessSent = true;
|
|
490
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
498
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
|
|
491
499
|
}
|
|
492
500
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
493
501
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
494
|
-
const instanceId = args.instance_id ??
|
|
495
|
-
const telemetryExtra =
|
|
502
|
+
const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
|
|
503
|
+
const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
|
|
496
504
|
if (name === 'recall_best_solution') {
|
|
497
505
|
// recall_best_solution → increments BrainRecallCount + triggers level-up logic
|
|
498
506
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
@@ -557,9 +565,9 @@ async function handleTool(name, args) {
|
|
|
557
565
|
// Delegate context tools (remember/recall/list/forget)
|
|
558
566
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
559
567
|
if (contextResult !== null) {
|
|
560
|
-
if (!_firstCallSuccessSent &&
|
|
568
|
+
if (!_firstCallSuccessSent && getJWT()) {
|
|
561
569
|
_firstCallSuccessSent = true;
|
|
562
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
570
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
|
|
563
571
|
}
|
|
564
572
|
return contextResult;
|
|
565
573
|
}
|
|
@@ -584,8 +592,8 @@ async function handleTool(name, args) {
|
|
|
584
592
|
if (syndicateResult !== null) {
|
|
585
593
|
// brain_predict is in syndicate handler — track telemetry here.
|
|
586
594
|
if (name === 'brain_predict') {
|
|
587
|
-
const instanceId = args.instance_id ??
|
|
588
|
-
const telemetryExtra =
|
|
595
|
+
const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
|
|
596
|
+
const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
|
|
589
597
|
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
590
598
|
}
|
|
591
599
|
return syndicateResult;
|
|
@@ -593,8 +601,8 @@ async function handleTool(name, args) {
|
|
|
593
601
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
594
602
|
if (fedbrainResult !== null) {
|
|
595
603
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
596
|
-
const instanceId = args.instance_id ??
|
|
597
|
-
const telemetryExtra =
|
|
604
|
+
const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
|
|
605
|
+
const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
|
|
598
606
|
if (name === 'brain_from_git') {
|
|
599
607
|
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
600
608
|
}
|
|
@@ -622,7 +630,7 @@ async function handleTool(name, args) {
|
|
|
622
630
|
healthStatus = `unreachable: ${e.message}`;
|
|
623
631
|
}
|
|
624
632
|
// Check JWT / auth
|
|
625
|
-
if (!
|
|
633
|
+
if (!getJWT()) {
|
|
626
634
|
return [
|
|
627
635
|
`📡 **cachly API Status**`,
|
|
628
636
|
``,
|
|
@@ -636,7 +644,7 @@ async function handleTool(name, args) {
|
|
|
636
644
|
// Decode JWT claims (inspection only, no verification)
|
|
637
645
|
let authInfo = '❌ invalid JWT format';
|
|
638
646
|
try {
|
|
639
|
-
const parts =
|
|
647
|
+
const parts = getJWT().split('.');
|
|
640
648
|
if (parts.length === 3) {
|
|
641
649
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
642
650
|
const sub = payload.sub ?? '(unknown)';
|
|
@@ -668,7 +676,7 @@ async function handleTool(name, args) {
|
|
|
668
676
|
const lines = ['\n\n🧠 **Brain Instances:**'];
|
|
669
677
|
for (const inst of instances) {
|
|
670
678
|
const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
|
|
671
|
-
const defaultMark = inst.id ===
|
|
679
|
+
const defaultMark = inst.id === getDefaultInstanceId() ? ' ← active' : '';
|
|
672
680
|
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
|
|
673
681
|
if (inst.status === 'running' && inst.host) {
|
|
674
682
|
// Try a quick ping on the cached connection
|
|
@@ -2022,7 +2030,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2022
2030
|
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
2023
2031
|
const CLIENT_ID = 'cachly-cli';
|
|
2024
2032
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2025
|
-
sendFunnelEvent('setup_auth_started');
|
|
2026
2033
|
// Start device flow
|
|
2027
2034
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
2028
2035
|
try {
|
|
@@ -2048,7 +2055,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2048
2055
|
rl.close();
|
|
2049
2056
|
process.exit(1);
|
|
2050
2057
|
}
|
|
2051
|
-
sendFunnelEvent('setup_auth_completed');
|
|
2052
2058
|
console.log('');
|
|
2053
2059
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2054
2060
|
}
|
|
@@ -2081,7 +2087,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2081
2087
|
if (tokenData.access_token) {
|
|
2082
2088
|
token = tokenData.access_token;
|
|
2083
2089
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2084
|
-
sendFunnelEvent('setup_auth_completed');
|
|
2085
2090
|
break;
|
|
2086
2091
|
}
|
|
2087
2092
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2216,7 +2221,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2216
2221
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2217
2222
|
}
|
|
2218
2223
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2219
|
-
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2220
2224
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2221
2225
|
const cwd = process.cwd();
|
|
2222
2226
|
const detected = [];
|
|
@@ -2465,7 +2469,7 @@ if (process.argv[2] === 'index') {
|
|
|
2465
2469
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2466
2470
|
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2467
2471
|
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2468
|
-
if (!instanceId || !
|
|
2472
|
+
if (!instanceId || !getJWT()) {
|
|
2469
2473
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2470
2474
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2471
2475
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2502,7 +2506,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2502
2506
|
const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
2503
2507
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2504
2508
|
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2505
|
-
if (!instanceId || !
|
|
2509
|
+
if (!instanceId || !getJWT()) {
|
|
2506
2510
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2507
2511
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2508
2512
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2534,7 +2538,8 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2534
2538
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2535
2539
|
// Skip for CLI commands that intentionally run without credentials.
|
|
2536
2540
|
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2537
|
-
|
|
2541
|
+
// In multi-tenant HTTP mode (PORT set) we never need a startup JWT — per-request auth.
|
|
2542
|
+
if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2538
2543
|
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2539
2544
|
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2540
2545
|
const banner = '\n' +
|
|
@@ -2601,9 +2606,39 @@ const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
|
2601
2606
|
if (httpPort) {
|
|
2602
2607
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2603
2608
|
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
2609
|
+
// Multi-tenant: per-request JWT + instance ID resolved via:
|
|
2610
|
+
// 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
|
|
2611
|
+
// 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
|
|
2612
|
+
// Falls back to module-level env-derived values when neither is provided.
|
|
2604
2613
|
const { createServer } = await import('node:http');
|
|
2605
2614
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2606
2615
|
await server.connect(transport);
|
|
2616
|
+
function extractReqCtx(req, url) {
|
|
2617
|
+
let jwtVal = '';
|
|
2618
|
+
let instId = '';
|
|
2619
|
+
const auth = req.headers['authorization'];
|
|
2620
|
+
if (typeof auth === 'string' && auth.toLowerCase().startsWith('bearer ')) {
|
|
2621
|
+
jwtVal = auth.slice(7).trim();
|
|
2622
|
+
}
|
|
2623
|
+
const hdrInst = req.headers['x-cachly-instance-id'];
|
|
2624
|
+
if (typeof hdrInst === 'string')
|
|
2625
|
+
instId = hdrInst.trim();
|
|
2626
|
+
// Smithery passes user config as a base64url-encoded JSON blob in ?config=
|
|
2627
|
+
const cfgParam = url.searchParams.get('config');
|
|
2628
|
+
if (cfgParam && (!jwtVal || !instId)) {
|
|
2629
|
+
try {
|
|
2630
|
+
const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
|
|
2631
|
+
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
|
2632
|
+
const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
2633
|
+
if (!jwtVal && typeof json.CACHLY_JWT === 'string')
|
|
2634
|
+
jwtVal = json.CACHLY_JWT;
|
|
2635
|
+
if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string')
|
|
2636
|
+
instId = json.CACHLY_BRAIN_INSTANCE_ID;
|
|
2637
|
+
}
|
|
2638
|
+
catch { /* malformed config — ignore, fall back to globals */ }
|
|
2639
|
+
}
|
|
2640
|
+
return { jwt: jwtVal, instanceId: instId };
|
|
2641
|
+
}
|
|
2607
2642
|
const httpServer = createServer((req, res) => {
|
|
2608
2643
|
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2609
2644
|
if (url.pathname === '/health') {
|
|
@@ -2611,6 +2646,8 @@ if (httpPort) {
|
|
|
2611
2646
|
res.end(JSON.stringify({
|
|
2612
2647
|
status: 'ok',
|
|
2613
2648
|
version: CURRENT_VERSION,
|
|
2649
|
+
transport: 'streamable-http',
|
|
2650
|
+
multiTenant: true,
|
|
2614
2651
|
zeroResults: {
|
|
2615
2652
|
total: zeroResultsTotal,
|
|
2616
2653
|
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
@@ -2619,7 +2656,16 @@ if (httpPort) {
|
|
|
2619
2656
|
return;
|
|
2620
2657
|
}
|
|
2621
2658
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2622
|
-
|
|
2659
|
+
const ctx = extractReqCtx(req, url);
|
|
2660
|
+
reqCtx.run(ctx, () => {
|
|
2661
|
+
transport.handleRequest(req, res).catch((err) => {
|
|
2662
|
+
process.stderr.write(`cachly-mcp HTTP handler error: ${err.message}\n`);
|
|
2663
|
+
if (!res.headersSent) {
|
|
2664
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2665
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
});
|
|
2623
2669
|
return;
|
|
2624
2670
|
}
|
|
2625
2671
|
res.writeHead(404);
|