@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"mcpName": "io.github.cachly-dev/cachly-mcp",
|
|
5
5
|
"description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 63 MCP tools · Claude Code · Cursor · GitHub Copilot · Windsurf · Free tier · EU servers.",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { Redis } from 'ioredis';
|
|
|
55
55
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
56
56
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
57
57
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
58
|
-
const CURRENT_VERSION = '0.9.
|
|
58
|
+
const CURRENT_VERSION = '0.9.8';
|
|
59
59
|
|
|
60
60
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
61
61
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -1960,18 +1960,20 @@ async function keywordSearch(
|
|
|
1960
1960
|
query: string,
|
|
1961
1961
|
topK = 10,
|
|
1962
1962
|
): Promise<KeywordMatch[]> {
|
|
1963
|
-
// ── Step 1: Collect
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
const stream = redis.scanStream({ match: pattern, count:
|
|
1967
|
-
|
|
1963
|
+
// ── Step 1: Collect all matching keys — patterns scanned in parallel ──
|
|
1964
|
+
const keyBatches = await Promise.all(patterns.map(pattern => {
|
|
1965
|
+
const keys: string[] = [];
|
|
1966
|
+
const stream = redis.scanStream({ match: pattern, count: 500 });
|
|
1967
|
+
return new Promise<string[]>((resolve, reject) => {
|
|
1968
1968
|
stream.on('data', (batch: string[]) => {
|
|
1969
|
-
|
|
1969
|
+
keys.push(...batch.filter((k: string) => !k.endsWith(':meta')));
|
|
1970
1970
|
});
|
|
1971
|
-
stream.on('end', resolve);
|
|
1971
|
+
stream.on('end', () => resolve(keys));
|
|
1972
1972
|
stream.on('error', reject);
|
|
1973
1973
|
});
|
|
1974
|
-
}
|
|
1974
|
+
}));
|
|
1975
|
+
// Deduplicate across patterns (a key may match multiple globs)
|
|
1976
|
+
const allKeys = [...new Set(keyBatches.flat())];
|
|
1975
1977
|
|
|
1976
1978
|
if (allKeys.length === 0) return [];
|
|
1977
1979
|
|
|
@@ -2285,15 +2287,37 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
2285
2287
|
|
|
2286
2288
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
2287
2289
|
|
|
2290
|
+
function jwtExpiryMs(token: string): number | null {
|
|
2291
|
+
try {
|
|
2292
|
+
const parts = token.split('.');
|
|
2293
|
+
if (parts.length !== 3) return null;
|
|
2294
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as { exp?: number };
|
|
2295
|
+
return payload.exp ? payload.exp * 1000 : null;
|
|
2296
|
+
} catch { return null; }
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2288
2299
|
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
2289
2300
|
if (!JWT) {
|
|
2290
2301
|
throw new McpError(
|
|
2291
2302
|
ErrorCode.InvalidRequest,
|
|
2292
|
-
'CACHLY_JWT env var not set
|
|
2303
|
+
'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>'
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// Detect token expiry before making the network call — gives a clearer error
|
|
2308
|
+
const expMs = jwtExpiryMs(JWT);
|
|
2309
|
+
if (expMs !== null && expMs < Date.now()) {
|
|
2310
|
+
const expiredAt = new Date(expMs).toISOString();
|
|
2311
|
+
throw new McpError(
|
|
2312
|
+
ErrorCode.InvalidRequest,
|
|
2313
|
+
`CACHLY_JWT expired at ${expiredAt}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`
|
|
2293
2314
|
);
|
|
2294
2315
|
}
|
|
2316
|
+
|
|
2317
|
+
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
2295
2318
|
const res = await fetch(`${API_URL}${path}`, {
|
|
2296
2319
|
...options,
|
|
2320
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
2297
2321
|
headers: {
|
|
2298
2322
|
'Content-Type': 'application/json',
|
|
2299
2323
|
Authorization: `Bearer ${JWT}`,
|
|
@@ -2302,9 +2326,22 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
|
|
2302
2326
|
});
|
|
2303
2327
|
if (!res.ok) {
|
|
2304
2328
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
2329
|
+
const detail = (body as { error?: string }).error ?? res.statusText;
|
|
2330
|
+
if (res.status === 401) {
|
|
2331
|
+
throw new McpError(
|
|
2332
|
+
ErrorCode.InvalidRequest,
|
|
2333
|
+
`Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
if (res.status === 403) {
|
|
2337
|
+
throw new McpError(
|
|
2338
|
+
ErrorCode.InvalidRequest,
|
|
2339
|
+
`Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2305
2342
|
throw new McpError(
|
|
2306
2343
|
ErrorCode.InternalError,
|
|
2307
|
-
`cachly API error ${res.status}: ${
|
|
2344
|
+
`cachly API error ${res.status}: ${detail}`
|
|
2308
2345
|
);
|
|
2309
2346
|
}
|
|
2310
2347
|
if (res.status === 204) return undefined as T;
|
|
@@ -2466,15 +2503,21 @@ const TOOLS = [
|
|
|
2466
2503
|
},
|
|
2467
2504
|
{
|
|
2468
2505
|
name: 'cache_delete',
|
|
2469
|
-
description:
|
|
2506
|
+
description:
|
|
2507
|
+
'Permanently delete one or more keys from a running cache instance (uses Redis DEL). ' +
|
|
2508
|
+
'This operation is destructive and irreversible — deleted keys cannot be recovered. ' +
|
|
2509
|
+
'Deleting a non-existent key is safe and returns 0 for that key (no error). ' +
|
|
2510
|
+
'Returns the count of keys that were actually deleted (existing keys only). ' +
|
|
2511
|
+
'Use this to explicitly remove stale entries; prefer cache_set with a short TTL for auto-expiring data. ' +
|
|
2512
|
+
'Do NOT use this to clear an entire instance — use the dashboard or delete_instance for that.',
|
|
2470
2513
|
inputSchema: {
|
|
2471
2514
|
type: 'object',
|
|
2472
2515
|
properties: {
|
|
2473
|
-
instance_id: { type: 'string' },
|
|
2516
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance to delete keys from (get from list_instances)' },
|
|
2474
2517
|
keys: {
|
|
2475
2518
|
type: 'array',
|
|
2476
2519
|
items: { type: 'string' },
|
|
2477
|
-
description: 'One or more cache keys to delete',
|
|
2520
|
+
description: 'One or more cache keys to delete. Accepts exact keys only (no glob patterns — use cache_keys to list first).',
|
|
2478
2521
|
},
|
|
2479
2522
|
},
|
|
2480
2523
|
required: ['instance_id', 'keys'],
|
|
@@ -2482,12 +2525,18 @@ const TOOLS = [
|
|
|
2482
2525
|
},
|
|
2483
2526
|
{
|
|
2484
2527
|
name: 'cache_exists',
|
|
2485
|
-
description:
|
|
2528
|
+
description:
|
|
2529
|
+
'Check whether one or more keys exist in a running cache instance (uses Redis EXISTS). ' +
|
|
2530
|
+
'Read-only — no side effects. Returns the count of keys that currently exist (integer 0 to N). ' +
|
|
2531
|
+
'If none of the keys exist, returns 0. If all exist, returns the total key count passed in. ' +
|
|
2532
|
+
'Duplicate keys in the input array are each counted separately (Redis behavior). ' +
|
|
2533
|
+
'Use this to check presence before a cache_get to avoid null handling, or to verify a cache warm-up completed. ' +
|
|
2534
|
+
'Use cache_get instead if you also need the value; use cache_ttl if you need expiry info.',
|
|
2486
2535
|
inputSchema: {
|
|
2487
2536
|
type: 'object',
|
|
2488
2537
|
properties: {
|
|
2489
|
-
instance_id: { type: 'string' },
|
|
2490
|
-
keys: { type: 'array', items: { type: 'string' }, description: 'Keys to check' },
|
|
2538
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance to check (get from list_instances)' },
|
|
2539
|
+
keys: { type: 'array', items: { type: 'string' }, description: 'Keys to check for existence. Accepts exact keys only (no glob patterns).' },
|
|
2491
2540
|
},
|
|
2492
2541
|
required: ['instance_id', 'keys'],
|
|
2493
2542
|
},
|
|
@@ -3346,8 +3395,12 @@ const TOOLS = [
|
|
|
3346
3395
|
{
|
|
3347
3396
|
name: 'global_recall',
|
|
3348
3397
|
description:
|
|
3349
|
-
'
|
|
3350
|
-
'Returns
|
|
3398
|
+
'Read-only retrieval of cross-project lessons stored via global_learn. No side effects. ' +
|
|
3399
|
+
'Returns a list of matching global lesson objects, each with topic, lesson text, severity, and tags. ' +
|
|
3400
|
+
'If no topic is provided, returns all global lessons (up to 50). ' +
|
|
3401
|
+
'If topic is provided, returns all lessons whose topic key contains that string (partial match). ' +
|
|
3402
|
+
'Use this for lessons that apply universally across all projects (tool quirks, shell gotchas, platform behavior). ' +
|
|
3403
|
+
'Use recall_best_solution instead for project-specific lessons; use team_recall for org-scoped lessons.',
|
|
3351
3404
|
inputSchema: {
|
|
3352
3405
|
type: 'object',
|
|
3353
3406
|
properties: {
|
|
@@ -10297,6 +10350,23 @@ if (!JWT) {
|
|
|
10297
10350
|
'╚══════════════════════════════════════════════════════════════════╝\n' +
|
|
10298
10351
|
'\n',
|
|
10299
10352
|
);
|
|
10353
|
+
} else {
|
|
10354
|
+
// Warn if the JWT is already expired or expiring within the hour.
|
|
10355
|
+
const expMs = jwtExpiryMs(JWT);
|
|
10356
|
+
if (expMs !== null) {
|
|
10357
|
+
const minsLeft = Math.floor((expMs - Date.now()) / 60_000);
|
|
10358
|
+
if (minsLeft <= 0) {
|
|
10359
|
+
process.stderr.write(
|
|
10360
|
+
`\n⚠️ cachly: CACHLY_JWT expired ${Math.abs(minsLeft)} minute(s) ago.\n` +
|
|
10361
|
+
` All tool calls will fail. Get a fresh token at https://cachly.dev/setup-ai\n\n`,
|
|
10362
|
+
);
|
|
10363
|
+
} else if (minsLeft < 60) {
|
|
10364
|
+
process.stderr.write(
|
|
10365
|
+
`\n⚠️ cachly: CACHLY_JWT expires in ${minsLeft} minute(s).\n` +
|
|
10366
|
+
` Refresh it soon at https://cachly.dev/setup-ai to avoid interruptions.\n\n`,
|
|
10367
|
+
);
|
|
10368
|
+
}
|
|
10369
|
+
}
|
|
10300
10370
|
}
|
|
10301
10371
|
|
|
10302
10372
|
// ── Update nudge (non-blocking, fire-and-forget) ─────────────────────────────
|