@cachly-dev/mcp-server 0.8.3 → 0.9.0
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/.github/workflows/publish.yml +42 -0
- package/CHANGELOG.md +16 -0
- package/Dockerfile +4 -2
- package/LICENSE +202 -0
- package/README.md +278 -172
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -1
- package/glama.json +6 -0
- package/package.json +10 -4
- package/server.json +3 -3
- package/src/index.ts +246 -0
- package/test-features.cjs +0 -23
package/glama.json
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"mcpName": "io.github.cachly-dev/cachly-mcp",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "The world's first Cognitive Cache — AI memory that thinks. 67 MCP tools: sessions, lessons, semantic search, team brain, knowledge syndication, causal tracing, memory consolidation, compact recovery. Claude Code, Cursor, Copilot, Windsurf.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"cachly-mcp": "dist/index.js"
|
|
8
|
+
"cachly-mcp": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"scripts": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"license": "Apache-2.0",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "
|
|
37
|
+
"url": "https://github.com/cachly-dev/cachly-mcp"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public",
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
"team-knowledge",
|
|
59
59
|
"ambient-git",
|
|
60
60
|
"memory-crystals",
|
|
61
|
+
"cognitive-cache",
|
|
62
|
+
"causal-reasoning",
|
|
63
|
+
"knowledge-graph",
|
|
64
|
+
"autopilot-memory",
|
|
65
|
+
"knowledge-syndication",
|
|
66
|
+
"collective-intelligence",
|
|
61
67
|
"cachly",
|
|
62
68
|
"redis",
|
|
63
69
|
"valkey",
|
package/server.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.cachly-dev/cachly-mcp",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "The world's first Cognitive Cache — AI memory that thinks. 67 MCP tools: persistent sessions, causal root-cause tracing, knowledge decay scoring, brain diff, memory consolidation, knowledge syndication, zero-config autopilot. Works with Claude, Cursor, Copilot, Windsurf, Gemini. German servers, GDPR-native.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/cachly-dev/cachly-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.9.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"name": "@cachly-dev/mcp-server",
|
|
14
14
|
"identifier": "@cachly-dev/mcp-server",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.9.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
package/src/index.ts
CHANGED
|
@@ -3556,6 +3556,62 @@ const TOOLS = [
|
|
|
3556
3556
|
required: [],
|
|
3557
3557
|
},
|
|
3558
3558
|
},
|
|
3559
|
+
// ── v0.9 — compact_recover ─────────────────────────────────────────────────
|
|
3560
|
+
{
|
|
3561
|
+
name: 'compact_recover',
|
|
3562
|
+
description:
|
|
3563
|
+
'FAST context recovery after Claude/Cursor/Copilot context window compaction. ' +
|
|
3564
|
+
'Call this as the FIRST tool call after any context limit hit or window restart — before reading any files. ' +
|
|
3565
|
+
'Returns: last checkpoint (task + files + next_step), session handoff if any, last 5 recalled lessons, open TODOs. ' +
|
|
3566
|
+
'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
|
|
3567
|
+
'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
|
|
3568
|
+
inputSchema: {
|
|
3569
|
+
type: 'object',
|
|
3570
|
+
properties: {
|
|
3571
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
3572
|
+
focus: {
|
|
3573
|
+
type: 'string',
|
|
3574
|
+
description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
|
|
3575
|
+
},
|
|
3576
|
+
},
|
|
3577
|
+
required: ['instance_id'],
|
|
3578
|
+
},
|
|
3579
|
+
},
|
|
3580
|
+
// ── v0.9 — brain_from_git ─────────────────────────────────────────────────
|
|
3581
|
+
{
|
|
3582
|
+
name: 'brain_from_git',
|
|
3583
|
+
description:
|
|
3584
|
+
'Bootstrap the AI Brain from git history — reads commit messages and automatically stores lessons. ' +
|
|
3585
|
+
'Scans the last N days of commits, extracts actionable knowledge (fixes, reverts, deploys, migrations), ' +
|
|
3586
|
+
'and bulk-loads them into the Brain as lessons. ' +
|
|
3587
|
+
'Run once to pre-load months of team knowledge before the first session_start. ' +
|
|
3588
|
+
'Skips commits already in the Brain (safe to re-run). ' +
|
|
3589
|
+
'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
|
|
3590
|
+
'→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
|
|
3591
|
+
inputSchema: {
|
|
3592
|
+
type: 'object',
|
|
3593
|
+
properties: {
|
|
3594
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
3595
|
+
workspace_path: {
|
|
3596
|
+
type: 'string',
|
|
3597
|
+
description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
|
|
3598
|
+
},
|
|
3599
|
+
days: {
|
|
3600
|
+
type: 'number',
|
|
3601
|
+
description: 'How many days of history to scan (default: 180, max: 365)',
|
|
3602
|
+
},
|
|
3603
|
+
max_commits: {
|
|
3604
|
+
type: 'number',
|
|
3605
|
+
description: 'Maximum number of commits to process (default: 200, max: 500)',
|
|
3606
|
+
},
|
|
3607
|
+
author: {
|
|
3608
|
+
type: 'string',
|
|
3609
|
+
description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
|
|
3610
|
+
},
|
|
3611
|
+
},
|
|
3612
|
+
required: ['instance_id', 'workspace_path'],
|
|
3613
|
+
},
|
|
3614
|
+
},
|
|
3559
3615
|
] as const;
|
|
3560
3616
|
|
|
3561
3617
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
@@ -7852,6 +7908,196 @@ smart_recall(
|
|
|
7852
7908
|
return lines.filter(l => l !== '').join('\n');
|
|
7853
7909
|
}
|
|
7854
7910
|
|
|
7911
|
+
// ── v0.9 compact_recover — fast context recovery ──────────────────────────
|
|
7912
|
+
case 'compact_recover': {
|
|
7913
|
+
const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
|
|
7914
|
+
const redis = await getConnection(instance_id);
|
|
7915
|
+
|
|
7916
|
+
const lines: string[] = ['⚡ **Context Recovered**', ''];
|
|
7917
|
+
|
|
7918
|
+
// 1. Latest checkpoint
|
|
7919
|
+
const checkpointRaw = await redis.get('cachly:session:checkpoint');
|
|
7920
|
+
if (checkpointRaw) {
|
|
7921
|
+
try {
|
|
7922
|
+
const cp = JSON.parse(checkpointRaw) as {
|
|
7923
|
+
ts: string; task: string; files_touched?: string[]; next_step?: string; provider?: string;
|
|
7924
|
+
};
|
|
7925
|
+
const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
|
|
7926
|
+
const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
|
|
7927
|
+
lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
|
|
7928
|
+
if (cp.files_touched && cp.files_touched.length > 0) {
|
|
7929
|
+
lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
|
|
7930
|
+
}
|
|
7931
|
+
if (cp.next_step) lines.push(`📍 **Resume with:** ${cp.next_step}`);
|
|
7932
|
+
lines.push('');
|
|
7933
|
+
} catch { /* ignore corrupt */ }
|
|
7934
|
+
}
|
|
7935
|
+
|
|
7936
|
+
// 2. Handoff (if any, from a different window)
|
|
7937
|
+
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
7938
|
+
if (handoffRaw) {
|
|
7939
|
+
try {
|
|
7940
|
+
const hf = JSON.parse(handoffRaw) as { ts: string; remaining_tasks?: string[]; blocked_on?: string };
|
|
7941
|
+
const remaining = hf.remaining_tasks ?? [];
|
|
7942
|
+
if (remaining.length > 0) {
|
|
7943
|
+
lines.push(`⏳ **Remaining tasks:**`);
|
|
7944
|
+
remaining.slice(0, 5).forEach(t => lines.push(` - ${t}`));
|
|
7945
|
+
if (remaining.length > 5) lines.push(` …and ${remaining.length - 5} more`);
|
|
7946
|
+
lines.push('');
|
|
7947
|
+
}
|
|
7948
|
+
if (hf.blocked_on) { lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, ''); }
|
|
7949
|
+
} catch { /* ignore */ }
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7952
|
+
// 3. Top recently recalled lessons (BM25 if focus, else recency)
|
|
7953
|
+
const lessonKeys: string[] = [];
|
|
7954
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
7955
|
+
await new Promise<void>((resolve, reject) => {
|
|
7956
|
+
lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
|
|
7957
|
+
lStream.on('end', resolve);
|
|
7958
|
+
lStream.on('error', reject);
|
|
7959
|
+
});
|
|
7960
|
+
|
|
7961
|
+
type RecoverLesson = { topic: string; outcome: string; what_worked: string; ts: string; recall_count?: number; tags?: string[] };
|
|
7962
|
+
const allLessons: RecoverLesson[] = [];
|
|
7963
|
+
for (const k of lessonKeys) {
|
|
7964
|
+
const raw = await redis.get(k);
|
|
7965
|
+
if (!raw) continue;
|
|
7966
|
+
try { allLessons.push(JSON.parse(raw) as RecoverLesson); } catch { /* skip */ }
|
|
7967
|
+
}
|
|
7968
|
+
|
|
7969
|
+
const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
|
|
7970
|
+
let topLessons = focusTerms.length > 0
|
|
7971
|
+
? allLessons.filter(l =>
|
|
7972
|
+
focusTerms.some(t => l.topic.toLowerCase().includes(t) || (l.tags ?? []).some((tag: string) => tag.toLowerCase().includes(t)))
|
|
7973
|
+
).slice(0, 5)
|
|
7974
|
+
: allLessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
|
|
7975
|
+
|
|
7976
|
+
if (topLessons.length > 0) {
|
|
7977
|
+
lines.push(`🧠 **Top lessons${focus ? ` for "${focus}"` : ''}:**`);
|
|
7978
|
+
for (const l of topLessons) {
|
|
7979
|
+
const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
|
|
7980
|
+
lines.push(` ${icon} \`${l.topic}\` — ${l.what_worked.slice(0, 120)}${l.what_worked.length > 120 ? '…' : ''}`);
|
|
7981
|
+
}
|
|
7982
|
+
lines.push('');
|
|
7983
|
+
}
|
|
7984
|
+
|
|
7985
|
+
lines.push(`💡 Call \`session_start\` for the full briefing, or resume work directly.`);
|
|
7986
|
+
return lines.filter(l => l !== '').join('\n');
|
|
7987
|
+
}
|
|
7988
|
+
|
|
7989
|
+
// ── v0.9 brain_from_git — bulk-load git history as lessons ───────────────
|
|
7990
|
+
case 'brain_from_git': {
|
|
7991
|
+
const {
|
|
7992
|
+
instance_id,
|
|
7993
|
+
workspace_path,
|
|
7994
|
+
days = 180,
|
|
7995
|
+
max_commits = 200,
|
|
7996
|
+
author = '',
|
|
7997
|
+
} = args as { instance_id: string; workspace_path: string; days?: number; max_commits?: number; author?: string };
|
|
7998
|
+
|
|
7999
|
+
const redis = await getConnection(instance_id);
|
|
8000
|
+
const { execSync } = await import('node:child_process');
|
|
8001
|
+
|
|
8002
|
+
const clampedDays = Math.min(Math.max(1, days), 365);
|
|
8003
|
+
const clampedMax = Math.min(Math.max(1, max_commits), 500);
|
|
8004
|
+
|
|
8005
|
+
const authorArg = author ? `--author="${author}"` : '';
|
|
8006
|
+
let gitRaw = '';
|
|
8007
|
+
try {
|
|
8008
|
+
gitRaw = execSync(
|
|
8009
|
+
`git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`,
|
|
8010
|
+
{ timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
8011
|
+
).trim();
|
|
8012
|
+
} catch {
|
|
8013
|
+
return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
if (!gitRaw) {
|
|
8017
|
+
return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
const commitLines = gitRaw.split('\n');
|
|
8021
|
+
const now = new Date();
|
|
8022
|
+
|
|
8023
|
+
const actionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|rewrite|deploy|feat|revert|break|merge|bump)\b/i;
|
|
8024
|
+
const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
|
|
8025
|
+
|
|
8026
|
+
let stored = 0;
|
|
8027
|
+
let skipped = 0;
|
|
8028
|
+
let failures = 0;
|
|
8029
|
+
const topLessons: string[] = [];
|
|
8030
|
+
|
|
8031
|
+
for (const line of commitLines) {
|
|
8032
|
+
const parts = line.split('|||');
|
|
8033
|
+
if (parts.length < 2) continue;
|
|
8034
|
+
const [hash, msg, dateStr] = parts;
|
|
8035
|
+
if (!msg || !actionRe.test(msg)) continue;
|
|
8036
|
+
|
|
8037
|
+
const isFail = failRe.test(msg);
|
|
8038
|
+
const cleanMsg = msg
|
|
8039
|
+
.replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
|
|
8040
|
+
.trim();
|
|
8041
|
+
if (!cleanMsg || cleanMsg.length < 5) continue;
|
|
8042
|
+
|
|
8043
|
+
const slug = cleanMsg
|
|
8044
|
+
.toLowerCase()
|
|
8045
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
8046
|
+
.replace(/^-+|-+$/g, '')
|
|
8047
|
+
.slice(0, 40);
|
|
8048
|
+
if (!slug) continue;
|
|
8049
|
+
|
|
8050
|
+
const topic = `git:${slug}`;
|
|
8051
|
+
const key = `cachly:lesson:best:${topic}`;
|
|
8052
|
+
const existing = await redis.get(key);
|
|
8053
|
+
if (existing) { skipped++; continue; }
|
|
8054
|
+
|
|
8055
|
+
const lesson = {
|
|
8056
|
+
topic,
|
|
8057
|
+
outcome: isFail ? ('failure' as const) : ('success' as const),
|
|
8058
|
+
what_worked: isFail ? '' : cleanMsg.slice(0, 300),
|
|
8059
|
+
what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
|
|
8060
|
+
context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
|
|
8061
|
+
severity: 'minor' as const,
|
|
8062
|
+
ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
|
|
8063
|
+
recall_count: 0,
|
|
8064
|
+
auto_learned: true,
|
|
8065
|
+
source: 'brain_from_git',
|
|
8066
|
+
version: 3,
|
|
8067
|
+
};
|
|
8068
|
+
|
|
8069
|
+
await redis.set(key, JSON.stringify(lesson));
|
|
8070
|
+
await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
|
|
8071
|
+
stored++;
|
|
8072
|
+
if (isFail) failures++;
|
|
8073
|
+
if (topLessons.length < 5) topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
|
|
8074
|
+
}
|
|
8075
|
+
|
|
8076
|
+
const lines: string[] = [
|
|
8077
|
+
`🧠 **Brain bootstrapped from git history**`,
|
|
8078
|
+
``,
|
|
8079
|
+
`📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
|
|
8080
|
+
`✅ **Success lessons stored:** ${stored - failures}`,
|
|
8081
|
+
`❌ **Failure/revert lessons:** ${failures}`,
|
|
8082
|
+
skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
|
|
8083
|
+
``,
|
|
8084
|
+
];
|
|
8085
|
+
|
|
8086
|
+
if (topLessons.length > 0) {
|
|
8087
|
+
lines.push(`**Top lessons extracted:**`);
|
|
8088
|
+
lines.push(...topLessons);
|
|
8089
|
+
if (stored > 5) lines.push(` …and ${stored - 5} more`);
|
|
8090
|
+
lines.push('');
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
lines.push(
|
|
8094
|
+
`💡 **Next step:** Call \`session_start(focus="...")\` to see these lessons in your briefing.`,
|
|
8095
|
+
`🔄 **Re-run anytime** — duplicates are safely skipped.`,
|
|
8096
|
+
);
|
|
8097
|
+
|
|
8098
|
+
return lines.filter(l => l !== '').join('\n');
|
|
8099
|
+
}
|
|
8100
|
+
|
|
7855
8101
|
default:
|
|
7856
8102
|
return null;
|
|
7857
8103
|
}
|
package/test-features.cjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
|
|
3
|
-
const src = fs.readFileSync('src/index.ts', 'utf8');
|
|
4
|
-
const initSrc = fs.readFileSync('../init/index.js', 'utf8');
|
|
5
|
-
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
6
|
-
|
|
7
|
-
let pass = 0, fail = 0;
|
|
8
|
-
function check(name, cond) {
|
|
9
|
-
if (cond) { console.log('PASS:', name); pass++; }
|
|
10
|
-
else { console.log('FAIL:', name); fail++; }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
check('Test 1 - Ambient git learning in session_end', src.includes('git log --since') || src.includes('Ambient git'));
|
|
14
|
-
check('Test 2a - author param in learn_from_attempts schema', src.includes('"author"') && src.includes('learn_from_attempts'));
|
|
15
|
-
check('Test 2b - Team Telepathy in session_start', src.includes('Team Telepathy') || src.includes('team telepathy'));
|
|
16
|
-
check('Test 3 - memory_crystalize tool', src.includes('memory_crystalize'));
|
|
17
|
-
check('Test 4 - init team detection (~/.cachly-teams.json)', initSrc.includes('cachly-teams.json'));
|
|
18
|
-
check('Test 5 - brain_doctor workspace_path param', src.includes('workspace_path') && src.includes('brain_doctor'));
|
|
19
|
-
check('Bugfix - Pre-Warning uses splice(2,0)', src.includes('splice(2, 0'));
|
|
20
|
-
check('Bugfix - No broken unshift in pre-warning', !src.includes('lines.unshift('));
|
|
21
|
-
check('MCP version is 0.5.26', pkg.version === '0.5.26');
|
|
22
|
-
|
|
23
|
-
console.log(`\n${pass}/${pass+fail} tests passed`);
|