@cachly-dev/mcp-server 0.9.7 → 0.9.9
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/dist/index.js +77 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +89 -19
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
|
|
|
47
47
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
48
48
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
49
49
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
50
|
-
const CURRENT_VERSION = '0.9.
|
|
50
|
+
const CURRENT_VERSION = '0.9.8';
|
|
51
51
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
52
52
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
53
53
|
// When neither is set, we auto-fetch the first running instance once per process.
|
|
@@ -2005,18 +2005,20 @@ function recencyBoost(timestampMs) {
|
|
|
2005
2005
|
* └─────────────────────────────────────────────────────────────────┘
|
|
2006
2006
|
*/
|
|
2007
2007
|
async function keywordSearch(redis, patterns, query, topK = 10) {
|
|
2008
|
-
// ── Step 1: Collect
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
const stream = redis.scanStream({ match: pattern, count:
|
|
2012
|
-
|
|
2008
|
+
// ── Step 1: Collect all matching keys — patterns scanned in parallel ──
|
|
2009
|
+
const keyBatches = await Promise.all(patterns.map(pattern => {
|
|
2010
|
+
const keys = [];
|
|
2011
|
+
const stream = redis.scanStream({ match: pattern, count: 500 });
|
|
2012
|
+
return new Promise((resolve, reject) => {
|
|
2013
2013
|
stream.on('data', (batch) => {
|
|
2014
|
-
|
|
2014
|
+
keys.push(...batch.filter((k) => !k.endsWith(':meta')));
|
|
2015
2015
|
});
|
|
2016
|
-
stream.on('end', resolve);
|
|
2016
|
+
stream.on('end', () => resolve(keys));
|
|
2017
2017
|
stream.on('error', reject);
|
|
2018
2018
|
});
|
|
2019
|
-
}
|
|
2019
|
+
}));
|
|
2020
|
+
// Deduplicate across patterns (a key may match multiple globs)
|
|
2021
|
+
const allKeys = [...new Set(keyBatches.flat())];
|
|
2020
2022
|
if (allKeys.length === 0)
|
|
2021
2023
|
return [];
|
|
2022
2024
|
// Pipeline GET for speed
|
|
@@ -2269,12 +2271,32 @@ async function getConnection(instance_id) {
|
|
|
2269
2271
|
return client;
|
|
2270
2272
|
}
|
|
2271
2273
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
2274
|
+
function jwtExpiryMs(token) {
|
|
2275
|
+
try {
|
|
2276
|
+
const parts = token.split('.');
|
|
2277
|
+
if (parts.length !== 3)
|
|
2278
|
+
return null;
|
|
2279
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
2280
|
+
return payload.exp ? payload.exp * 1000 : null;
|
|
2281
|
+
}
|
|
2282
|
+
catch {
|
|
2283
|
+
return null;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2272
2286
|
async function apiFetch(path, options = {}) {
|
|
2273
2287
|
if (!JWT) {
|
|
2274
|
-
throw new McpError(ErrorCode.InvalidRequest, 'CACHLY_JWT env var not set
|
|
2288
|
+
throw new McpError(ErrorCode.InvalidRequest, 'CACHLY_JWT env var not set.\n\nGet your token at https://cachly.dev/setup-ai and add it to your MCP config:\n CACHLY_JWT=<your-token>');
|
|
2289
|
+
}
|
|
2290
|
+
// Detect token expiry before making the network call — gives a clearer error
|
|
2291
|
+
const expMs = jwtExpiryMs(JWT);
|
|
2292
|
+
if (expMs !== null && expMs < Date.now()) {
|
|
2293
|
+
const expiredAt = new Date(expMs).toISOString();
|
|
2294
|
+
throw new McpError(ErrorCode.InvalidRequest, `CACHLY_JWT expired at ${expiredAt}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`);
|
|
2275
2295
|
}
|
|
2296
|
+
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
2276
2297
|
const res = await fetch(`${API_URL}${path}`, {
|
|
2277
2298
|
...options,
|
|
2299
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
2278
2300
|
headers: {
|
|
2279
2301
|
'Content-Type': 'application/json',
|
|
2280
2302
|
Authorization: `Bearer ${JWT}`,
|
|
@@ -2283,7 +2305,14 @@ async function apiFetch(path, options = {}) {
|
|
|
2283
2305
|
});
|
|
2284
2306
|
if (!res.ok) {
|
|
2285
2307
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
2286
|
-
|
|
2308
|
+
const detail = body.error ?? res.statusText;
|
|
2309
|
+
if (res.status === 401) {
|
|
2310
|
+
throw new McpError(ErrorCode.InvalidRequest, `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`);
|
|
2311
|
+
}
|
|
2312
|
+
if (res.status === 403) {
|
|
2313
|
+
throw new McpError(ErrorCode.InvalidRequest, `Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`);
|
|
2314
|
+
}
|
|
2315
|
+
throw new McpError(ErrorCode.InternalError, `cachly API error ${res.status}: ${detail}`);
|
|
2287
2316
|
}
|
|
2288
2317
|
if (res.status === 204)
|
|
2289
2318
|
return undefined;
|
|
@@ -2423,15 +2452,20 @@ const TOOLS = [
|
|
|
2423
2452
|
},
|
|
2424
2453
|
{
|
|
2425
2454
|
name: 'cache_delete',
|
|
2426
|
-
description: '
|
|
2455
|
+
description: 'Permanently delete one or more keys from a running cache instance (uses Redis DEL). ' +
|
|
2456
|
+
'This operation is destructive and irreversible — deleted keys cannot be recovered. ' +
|
|
2457
|
+
'Deleting a non-existent key is safe and returns 0 for that key (no error). ' +
|
|
2458
|
+
'Returns the count of keys that were actually deleted (existing keys only). ' +
|
|
2459
|
+
'Use this to explicitly remove stale entries; prefer cache_set with a short TTL for auto-expiring data. ' +
|
|
2460
|
+
'Do NOT use this to clear an entire instance — use the dashboard or delete_instance for that.',
|
|
2427
2461
|
inputSchema: {
|
|
2428
2462
|
type: 'object',
|
|
2429
2463
|
properties: {
|
|
2430
|
-
instance_id: { type: 'string' },
|
|
2464
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance to delete keys from (get from list_instances)' },
|
|
2431
2465
|
keys: {
|
|
2432
2466
|
type: 'array',
|
|
2433
2467
|
items: { type: 'string' },
|
|
2434
|
-
description: 'One or more cache keys to delete',
|
|
2468
|
+
description: 'One or more cache keys to delete. Accepts exact keys only (no glob patterns — use cache_keys to list first).',
|
|
2435
2469
|
},
|
|
2436
2470
|
},
|
|
2437
2471
|
required: ['instance_id', 'keys'],
|
|
@@ -2439,12 +2473,17 @@ const TOOLS = [
|
|
|
2439
2473
|
},
|
|
2440
2474
|
{
|
|
2441
2475
|
name: 'cache_exists',
|
|
2442
|
-
description: 'Check whether one or more keys exist in
|
|
2476
|
+
description: 'Check whether one or more keys exist in a running cache instance (uses Redis EXISTS). ' +
|
|
2477
|
+
'Read-only — no side effects. Returns the count of keys that currently exist (integer 0 to N). ' +
|
|
2478
|
+
'If none of the keys exist, returns 0. If all exist, returns the total key count passed in. ' +
|
|
2479
|
+
'Duplicate keys in the input array are each counted separately (Redis behavior). ' +
|
|
2480
|
+
'Use this to check presence before a cache_get to avoid null handling, or to verify a cache warm-up completed. ' +
|
|
2481
|
+
'Use cache_get instead if you also need the value; use cache_ttl if you need expiry info.',
|
|
2443
2482
|
inputSchema: {
|
|
2444
2483
|
type: 'object',
|
|
2445
2484
|
properties: {
|
|
2446
|
-
instance_id: { type: 'string' },
|
|
2447
|
-
keys: { type: 'array', items: { type: 'string' }, description: 'Keys to check' },
|
|
2485
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance to check (get from list_instances)' },
|
|
2486
|
+
keys: { type: 'array', items: { type: 'string' }, description: 'Keys to check for existence. Accepts exact keys only (no glob patterns).' },
|
|
2448
2487
|
},
|
|
2449
2488
|
required: ['instance_id', 'keys'],
|
|
2450
2489
|
},
|
|
@@ -3265,8 +3304,12 @@ const TOOLS = [
|
|
|
3265
3304
|
},
|
|
3266
3305
|
{
|
|
3267
3306
|
name: 'global_recall',
|
|
3268
|
-
description: '
|
|
3269
|
-
'Returns
|
|
3307
|
+
description: 'Read-only retrieval of cross-project lessons stored via global_learn. No side effects. ' +
|
|
3308
|
+
'Returns a list of matching global lesson objects, each with topic, lesson text, severity, and tags. ' +
|
|
3309
|
+
'If no topic is provided, returns all global lessons (up to 50). ' +
|
|
3310
|
+
'If topic is provided, returns all lessons whose topic key contains that string (partial match). ' +
|
|
3311
|
+
'Use this for lessons that apply universally across all projects (tool quirks, shell gotchas, platform behavior). ' +
|
|
3312
|
+
'Use recall_best_solution instead for project-specific lessons; use team_recall for org-scoped lessons.',
|
|
3270
3313
|
inputSchema: {
|
|
3271
3314
|
type: 'object',
|
|
3272
3315
|
properties: {
|
|
@@ -9523,6 +9566,21 @@ if (!JWT) {
|
|
|
9523
9566
|
'╚══════════════════════════════════════════════════════════════════╝\n' +
|
|
9524
9567
|
'\n');
|
|
9525
9568
|
}
|
|
9569
|
+
else {
|
|
9570
|
+
// Warn if the JWT is already expired or expiring within the hour.
|
|
9571
|
+
const expMs = jwtExpiryMs(JWT);
|
|
9572
|
+
if (expMs !== null) {
|
|
9573
|
+
const minsLeft = Math.floor((expMs - Date.now()) / 60_000);
|
|
9574
|
+
if (minsLeft <= 0) {
|
|
9575
|
+
process.stderr.write(`\n⚠️ cachly: CACHLY_JWT expired ${Math.abs(minsLeft)} minute(s) ago.\n` +
|
|
9576
|
+
` All tool calls will fail. Get a fresh token at https://cachly.dev/setup-ai\n\n`);
|
|
9577
|
+
}
|
|
9578
|
+
else if (minsLeft < 60) {
|
|
9579
|
+
process.stderr.write(`\n⚠️ cachly: CACHLY_JWT expires in ${minsLeft} minute(s).\n` +
|
|
9580
|
+
` Refresh it soon at https://cachly.dev/setup-ai to avoid interruptions.\n\n`);
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
}
|
|
9526
9584
|
// ── Update nudge (non-blocking, fire-and-forget) ─────────────────────────────
|
|
9527
9585
|
// Check npm registry once per process start; if outdated, log to stderr so
|
|
9528
9586
|
// the editor's MCP log shows an actionable one-liner. Skipped if opted out.
|