@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 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.7';
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 and tokenize all documents ──
2009
- const allKeys = [];
2010
- for (const pattern of patterns) {
2011
- const stream = redis.scanStream({ match: pattern, count: 200 });
2012
- await new Promise((resolve, reject) => {
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
- allKeys.push(...batch.filter((k) => !k.endsWith(':meta')));
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. Get your API token from https://cachly.dev/settings');
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
- throw new McpError(ErrorCode.InternalError, `cachly API error ${res.status}: ${body.error ?? res.statusText}`);
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: 'Delete one or more keys from a running cache instance.',
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 the cache. Returns a count of existing keys.',
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: 'Recall cross-project lessons stored via global_learn. ' +
3269
- 'Returns all global lessons or those matching a topic filter.',
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.