@cachly-dev/mcp-server 0.10.47 → 0.10.48
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 +33 -5
- package/dist/__tests__/brain-flow.test.js +2 -2
- package/dist/__tests__/brain-flow.test.js.map +1 -1
- package/dist/__tests__/handlers-unit.test.js +3 -1
- package/dist/__tests__/handlers-unit.test.js.map +1 -1
- package/dist/handlers/brain.js +1 -1
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/fedbrain.js +6 -5
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/roadmap.js +1 -1
- package/dist/handlers/roadmap.js.map +1 -1
- package/dist/handlers/team.js +1 -1
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +130 -17
- package/dist/index.js.map +1 -1
- package/dist/search.js +3 -3
- package/dist/search.js.map +1 -1
- package/package.json +2 -1
- package/smithery.yaml +0 -2
- package/src/__tests__/brain-flow.test.ts +2 -2
- package/src/__tests__/handlers-unit.test.ts +2 -1
- package/src/handlers/brain.ts +1 -1
- package/src/handlers/fedbrain.ts +6 -5
- package/src/handlers/roadmap.ts +1 -1
- package/src/handlers/team.ts +1 -1
- package/src/index.ts +82 -11
- package/src/search.ts +3 -3
package/dist/index.js
CHANGED
|
@@ -241,8 +241,19 @@ async function getConnection(instance_id) {
|
|
|
241
241
|
'• Pass instance_id explicitly to the tool, or\n' +
|
|
242
242
|
'• Run `list_instances` to see your available instances.');
|
|
243
243
|
}
|
|
244
|
-
if (pool.has(instance_id))
|
|
245
|
-
|
|
244
|
+
if (pool.has(instance_id)) {
|
|
245
|
+
const cached = pool.get(instance_id);
|
|
246
|
+
// Verify the pooled connection is still alive before reusing it.
|
|
247
|
+
// ioredis 'error' eviction is async — a connection can be in the pool
|
|
248
|
+
// but already internally broken (status 'reconnecting' or 'end').
|
|
249
|
+
const status = cached.status;
|
|
250
|
+
if (status === 'ready' || status === 'connect') {
|
|
251
|
+
return cached;
|
|
252
|
+
}
|
|
253
|
+
// Stale — evict and fall through to reconnect.
|
|
254
|
+
pool.delete(instance_id);
|
|
255
|
+
cached.disconnect();
|
|
256
|
+
}
|
|
246
257
|
// Fetch instance, waiting up to 25 s if it is still provisioning.
|
|
247
258
|
// This covers the zero-friction path: device-flow auth → auto-provision → first tool call
|
|
248
259
|
// all happen in quick succession and the instance isn't running yet.
|
|
@@ -319,12 +330,42 @@ async function getConnection(instance_id) {
|
|
|
319
330
|
const evict = () => pool.delete(instance_id);
|
|
320
331
|
client.on('error', evict);
|
|
321
332
|
client.on('end', evict);
|
|
322
|
-
|
|
323
|
-
|
|
333
|
+
// Retry up to 3 times with 2s backoff. Covers the window between K8s PodReady
|
|
334
|
+
// and Valkey actually accepting connections (RDB/AOF load, ~1-3s on cold start).
|
|
335
|
+
let connectErr;
|
|
336
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
337
|
+
try {
|
|
338
|
+
if (attempt > 0) {
|
|
339
|
+
await new Promise(r => setTimeout(r, 2000 * attempt));
|
|
340
|
+
// Fresh client on retry — ioredis doesn't allow reconnect after explicit disconnect.
|
|
341
|
+
client.disconnect();
|
|
342
|
+
const retryClient = new Redis({
|
|
343
|
+
host: inst.host,
|
|
344
|
+
port: inst.port,
|
|
345
|
+
password: password || undefined,
|
|
346
|
+
...(tlsEnabled ? { tls: {} } : {}),
|
|
347
|
+
lazyConnect: true,
|
|
348
|
+
enableReadyCheck: true,
|
|
349
|
+
connectTimeout: 8000,
|
|
350
|
+
retryStrategy: () => null,
|
|
351
|
+
});
|
|
352
|
+
retryClient.on('error', () => pool.delete(instance_id));
|
|
353
|
+
retryClient.on('end', () => pool.delete(instance_id));
|
|
354
|
+
await retryClient.connect();
|
|
355
|
+
pool.set(instance_id, retryClient);
|
|
356
|
+
return retryClient;
|
|
357
|
+
}
|
|
358
|
+
await client.connect();
|
|
359
|
+
connectErr = undefined;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
connectErr = err;
|
|
364
|
+
}
|
|
324
365
|
}
|
|
325
|
-
|
|
366
|
+
if (connectErr) {
|
|
326
367
|
client.disconnect();
|
|
327
|
-
const msg =
|
|
368
|
+
const msg = connectErr instanceof Error ? connectErr.message : String(connectErr);
|
|
328
369
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
329
370
|
method: 'POST',
|
|
330
371
|
headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
|
|
@@ -2603,6 +2644,27 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
2603
2644
|
})();
|
|
2604
2645
|
}
|
|
2605
2646
|
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
2647
|
+
// ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
|
|
2648
|
+
// SDK ≥1.9 forbids reusing a stateless transport across requests.
|
|
2649
|
+
// Solution: create a fresh Server+Transport per /mcp request.
|
|
2650
|
+
// The module-level `server` below is used for stdio mode only.
|
|
2651
|
+
function createHttpMcpServer() {
|
|
2652
|
+
const srv = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
|
|
2653
|
+
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
2654
|
+
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2655
|
+
const { name, arguments: args } = request.params;
|
|
2656
|
+
try {
|
|
2657
|
+
const text = await handleTool(name, (args ?? {}));
|
|
2658
|
+
return { content: [{ type: 'text', text }] };
|
|
2659
|
+
}
|
|
2660
|
+
catch (err) {
|
|
2661
|
+
if (err instanceof McpError)
|
|
2662
|
+
throw err;
|
|
2663
|
+
throw new McpError(ErrorCode.InternalError, err.message);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
return srv;
|
|
2667
|
+
}
|
|
2606
2668
|
if (httpPort) {
|
|
2607
2669
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2608
2670
|
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
@@ -2611,14 +2673,19 @@ if (httpPort) {
|
|
|
2611
2673
|
// 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
|
|
2612
2674
|
// Falls back to module-level env-derived values when neither is provided.
|
|
2613
2675
|
const { createServer } = await import('node:http');
|
|
2614
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2615
|
-
await server.connect(transport);
|
|
2616
2676
|
function extractReqCtx(req, url) {
|
|
2617
2677
|
let jwtVal = '';
|
|
2618
2678
|
let instId = '';
|
|
2619
2679
|
const auth = req.headers['authorization'];
|
|
2620
|
-
if (typeof auth === 'string' && auth.
|
|
2621
|
-
|
|
2680
|
+
if (typeof auth === 'string' && auth.trim()) {
|
|
2681
|
+
const a = auth.trim();
|
|
2682
|
+
jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
|
|
2683
|
+
}
|
|
2684
|
+
// Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
|
|
2685
|
+
if (!jwtVal) {
|
|
2686
|
+
const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
|
|
2687
|
+
if (typeof rawJwt === 'string')
|
|
2688
|
+
jwtVal = rawJwt.trim();
|
|
2622
2689
|
}
|
|
2623
2690
|
const hdrInst = req.headers['x-cachly-instance-id'];
|
|
2624
2691
|
if (typeof hdrInst === 'string')
|
|
@@ -2637,6 +2704,17 @@ if (httpPort) {
|
|
|
2637
2704
|
}
|
|
2638
2705
|
catch { /* malformed config — ignore, fall back to globals */ }
|
|
2639
2706
|
}
|
|
2707
|
+
// Direct query params (Smithery "query" transport, manual clients, etc.)
|
|
2708
|
+
if (!jwtVal) {
|
|
2709
|
+
const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
|
|
2710
|
+
if (q)
|
|
2711
|
+
jwtVal = q.trim();
|
|
2712
|
+
}
|
|
2713
|
+
if (!instId) {
|
|
2714
|
+
const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
|
|
2715
|
+
if (q)
|
|
2716
|
+
instId = q.trim();
|
|
2717
|
+
}
|
|
2640
2718
|
return { jwt: jwtVal, instanceId: instId };
|
|
2641
2719
|
}
|
|
2642
2720
|
const httpServer = createServer((req, res) => {
|
|
@@ -2655,16 +2733,51 @@ if (httpPort) {
|
|
|
2655
2733
|
}));
|
|
2656
2734
|
return;
|
|
2657
2735
|
}
|
|
2736
|
+
// Static server card — lets Smithery skip live scanning (avoids auth complexity).
|
|
2737
|
+
if (url.pathname === '/.well-known/mcp/server-card.json') {
|
|
2738
|
+
const card = {
|
|
2739
|
+
name: 'cachly AI Brain',
|
|
2740
|
+
version: CURRENT_VERSION,
|
|
2741
|
+
description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
|
|
2742
|
+
transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
|
|
2743
|
+
capabilities: { tools: {} },
|
|
2744
|
+
};
|
|
2745
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2746
|
+
res.end(JSON.stringify(card));
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2658
2749
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2750
|
+
// Handle CORS preflight
|
|
2751
|
+
if (req.method === 'OPTIONS') {
|
|
2752
|
+
res.writeHead(204, {
|
|
2753
|
+
'Access-Control-Allow-Origin': '*',
|
|
2754
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
2755
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
|
|
2756
|
+
});
|
|
2757
|
+
res.end();
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2659
2760
|
const ctx = extractReqCtx(req, url);
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2761
|
+
// Create a fresh Server+Transport for each request (stateless multi-tenant mode).
|
|
2762
|
+
// SDK ≥1.9 throws if a stateless transport is reused across requests.
|
|
2763
|
+
const reqServer = createHttpMcpServer();
|
|
2764
|
+
const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2765
|
+
reqServer.connect(reqTransport).then(() => {
|
|
2766
|
+
reqCtx.run(ctx, () => {
|
|
2767
|
+
reqTransport.handleRequest(req, res).catch((err) => {
|
|
2768
|
+
process.stderr.write(`cachly-mcp HTTP handler error: ${err.message}\n`);
|
|
2769
|
+
if (!res.headersSent) {
|
|
2770
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2771
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2667
2774
|
});
|
|
2775
|
+
}).catch((err) => {
|
|
2776
|
+
process.stderr.write(`cachly-mcp connect error: ${err.message}\n`);
|
|
2777
|
+
if (!res.headersSent) {
|
|
2778
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2779
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2780
|
+
}
|
|
2668
2781
|
});
|
|
2669
2782
|
return;
|
|
2670
2783
|
}
|