@cachly-dev/mcp-server 0.5.33 → 0.5.35
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/Dockerfile +11 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +201 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/smithery.yaml +10 -5
- package/src/index.ts +205 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.35",
|
|
4
4
|
"mcpName": "io.github.cachly-dev/mcp-server",
|
|
5
5
|
"description": "AI memory brain for Claude Code, Cursor, Copilot & Windsurf — persistent sessions, lessons learned, semantic search, team brain. 51 MCP tools.",
|
|
6
6
|
"type": "module",
|
package/smithery.yaml
CHANGED
|
@@ -5,16 +5,21 @@ startCommand:
|
|
|
5
5
|
properties:
|
|
6
6
|
CACHLY_JWT:
|
|
7
7
|
type: string
|
|
8
|
-
title: "API Key
|
|
9
|
-
description: "Your Cachly API key
|
|
8
|
+
title: "API Key"
|
|
9
|
+
description: "Your Cachly API key. Get it free at cachly.dev/setup-ai → Dashboard → API Keys. Looks like: cky_live_..."
|
|
10
|
+
examples:
|
|
11
|
+
- "cky_live_abc123..."
|
|
10
12
|
CACHLY_BRAIN_INSTANCE_ID:
|
|
11
13
|
type: string
|
|
12
14
|
title: "Brain Instance ID"
|
|
13
|
-
description: "UUID of your Cachly Brain instance
|
|
15
|
+
description: "UUID of your Cachly Brain instance. Find it at cachly.dev/instances — click your instance and copy the ID from the URL or details panel. Looks like: 9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
|
|
16
|
+
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
17
|
+
examples:
|
|
18
|
+
- "9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
|
|
14
19
|
CACHLY_API_URL:
|
|
15
20
|
type: string
|
|
16
|
-
title: "API URL"
|
|
17
|
-
description: "Cachly API endpoint — leave as default"
|
|
21
|
+
title: "API URL (optional)"
|
|
22
|
+
description: "Cachly API endpoint — leave as default unless you self-host"
|
|
18
23
|
default: "https://api.cachly.dev"
|
|
19
24
|
required:
|
|
20
25
|
- CACHLY_JWT
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { join, relative, extname } from 'node:path';
|
|
|
41
41
|
|
|
42
42
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
43
43
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
44
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
44
45
|
import {
|
|
45
46
|
CallToolRequestSchema,
|
|
46
47
|
ErrorCode,
|
|
@@ -300,13 +301,15 @@ function embedProviderHint(): string {
|
|
|
300
301
|
// • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
|
|
301
302
|
// • Multi-query splitting — numbered lists, semicolons, conjunctions
|
|
302
303
|
// • Fuzzy matching with Levenshtein distance ≤ 2
|
|
303
|
-
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT)
|
|
304
|
+
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO)
|
|
305
|
+
// • CJK character bigram extraction — handles Chinese, Japanese, Korean
|
|
304
306
|
// • Pipeline Redis reads for performance
|
|
305
307
|
//
|
|
306
308
|
|
|
307
309
|
/**
|
|
308
310
|
* Stopwords — filtered out during tokenization.
|
|
309
|
-
* Covers: English, German, French, Spanish, Italian, Portuguese
|
|
311
|
+
* Covers: English, German, French, Spanish, Italian, Portuguese,
|
|
312
|
+
* Chinese (Simplified + Traditional), Japanese, Korean.
|
|
310
313
|
* Keeps the index small and scores meaningful.
|
|
311
314
|
*/
|
|
312
315
|
const STOPWORDS = new Set([
|
|
@@ -380,15 +383,63 @@ const STOPWORDS = new Set([
|
|
|
380
383
|
'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
|
|
381
384
|
'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
|
|
382
385
|
'todo', 'toda', 'todos', 'cada', 'outro', 'outra', 'quando', 'porque',
|
|
386
|
+
// ── Chinese (Simplified + Traditional) — high-frequency function characters ──
|
|
387
|
+
'的', '了', '是', '在', '不', '和', '我', '他', '这', '中', '大', '为',
|
|
388
|
+
'上', '个', '国', '以', '要', '就', '出', '说', '们', '有', '来', '到',
|
|
389
|
+
'时', '地', '年', '得', '着', '那', '过', '后', '还', '与', '也', '可',
|
|
390
|
+
'于', '从', '但', '而', '被', '把', '让', '使', '对', '很', '都', '一',
|
|
391
|
+
'会', '没', '人', '它', '这个', '那个', '什么', '如果', '因为', '所以',
|
|
392
|
+
'已经', '可以', '这些', '那些', '我们', '他们', '她们', '它们',
|
|
393
|
+
// ── Japanese — common hiragana particles and auxiliary verbs ──
|
|
394
|
+
'の', 'は', 'が', 'を', 'に', 'で', 'と', 'も', 'や', 'か', 'な', 'ね',
|
|
395
|
+
'よ', 'わ', 'て', 'い', 'う', 'え', 'お', 'き', 'く', 'け', 'こ', 'さ',
|
|
396
|
+
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'ぬ', 'ん', 'から', 'まで',
|
|
397
|
+
'より', 'へ', 'です', 'ます', 'ない', 'ある', 'いる', 'する', 'こと',
|
|
398
|
+
'もの', 'ので', 'では', 'には', 'との', 'への', 'から', 'まで',
|
|
399
|
+
// ── Korean — common particles and auxiliary forms ──
|
|
400
|
+
'이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
|
|
401
|
+
'로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
|
|
402
|
+
'이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
|
|
383
403
|
]);
|
|
384
404
|
|
|
385
|
-
|
|
405
|
+
// CJK Unicode ranges used for bigram extraction
|
|
406
|
+
const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
|
|
407
|
+
const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Tokenize text into meaningful keywords.
|
|
411
|
+
* Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
|
|
412
|
+
* CJK text is split into character bigrams (the standard for indexing logographic languages).
|
|
413
|
+
*/
|
|
386
414
|
function tokenize(text: string): string[] {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
415
|
+
const tokens: string[] = [];
|
|
416
|
+
const lower = text.toLowerCase();
|
|
417
|
+
|
|
418
|
+
for (const [seg] of lower.matchAll(SEGMENT_RE)) {
|
|
419
|
+
if (CJK_RE.test(seg)) {
|
|
420
|
+
// CJK segment — extract overlapping character bigrams
|
|
421
|
+
for (let i = 0; i < seg.length - 1; i++) {
|
|
422
|
+
const bigram = seg[i] + seg[i + 1];
|
|
423
|
+
if (!STOPWORDS.has(seg[i]) && !STOPWORDS.has(bigram)) {
|
|
424
|
+
tokens.push(bigram);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Trailing single character (for 1-char CJK terms)
|
|
428
|
+
if (seg.length >= 1) {
|
|
429
|
+
const last = seg[seg.length - 1];
|
|
430
|
+
if (!STOPWORDS.has(last)) tokens.push(last);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
// Latin / other — existing logic
|
|
434
|
+
const words = seg
|
|
435
|
+
.replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
|
|
436
|
+
.split(/\s+/)
|
|
437
|
+
.filter(w => w.length > 1 && !STOPWORDS.has(w));
|
|
438
|
+
tokens.push(...words);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return tokens;
|
|
392
443
|
}
|
|
393
444
|
|
|
394
445
|
/**
|
|
@@ -5207,6 +5258,8 @@ const EDITOR_FILES: Record<string, string> = {
|
|
|
5207
5258
|
windsurf: '.windsurf/mcp.json',
|
|
5208
5259
|
copilot: '.vscode/mcp.json',
|
|
5209
5260
|
continue: '.continue/config.json',
|
|
5261
|
+
cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
|
|
5262
|
+
zed: '.zed/settings.json', // Zed project-level context_servers
|
|
5210
5263
|
};
|
|
5211
5264
|
|
|
5212
5265
|
const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
@@ -5223,6 +5276,20 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
|
|
|
5223
5276
|
},
|
|
5224
5277
|
}, null, 2);
|
|
5225
5278
|
}
|
|
5279
|
+
if (editor === 'zed') {
|
|
5280
|
+
return JSON.stringify({
|
|
5281
|
+
context_servers: {
|
|
5282
|
+
cachly: {
|
|
5283
|
+
command: {
|
|
5284
|
+
path: 'npx',
|
|
5285
|
+
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
5286
|
+
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
5287
|
+
},
|
|
5288
|
+
settings: {},
|
|
5289
|
+
},
|
|
5290
|
+
},
|
|
5291
|
+
}, null, 2);
|
|
5292
|
+
}
|
|
5226
5293
|
return JSON.stringify({
|
|
5227
5294
|
mcpServers: {
|
|
5228
5295
|
cachly: {
|
|
@@ -5555,15 +5622,28 @@ if (process.argv[2] === 'setup') {
|
|
|
5555
5622
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
5556
5623
|
const cwd = process.cwd();
|
|
5557
5624
|
const detected: string[] = [];
|
|
5625
|
+
const { homedir } = await import('node:os');
|
|
5626
|
+
const home = homedir();
|
|
5558
5627
|
// Claude Code: always include (CLAUDE.md is universal)
|
|
5559
5628
|
detected.push('claude');
|
|
5560
5629
|
if (existsSync(resolve(cwd, '.cursor'))) detected.push('cursor');
|
|
5561
5630
|
if (existsSync(resolve(cwd, '.windsurf'))) detected.push('windsurf');
|
|
5562
5631
|
if (existsSync(resolve(cwd, '.vscode'))) detected.push('copilot');
|
|
5563
5632
|
if (existsSync(resolve(cwd, '.continue'))) detected.push('continue');
|
|
5633
|
+
// Cline: VS Code extension — detect via globalStorage on the machine
|
|
5634
|
+
const clineGlobalDir = process.platform === 'darwin'
|
|
5635
|
+
? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
|
|
5636
|
+
: resolve(home, '.vscode/extensions');
|
|
5637
|
+
if (existsSync(clineGlobalDir) && !detected.includes('copilot')) detected.push('cline');
|
|
5638
|
+
else if (existsSync(clineGlobalDir)) detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
|
|
5639
|
+
// Zed editor — detect via app data dir
|
|
5640
|
+
const zedDir = process.platform === 'darwin'
|
|
5641
|
+
? resolve(home, 'Library/Application Support/Zed')
|
|
5642
|
+
: resolve(home, '.config/zed');
|
|
5643
|
+
if (existsSync(zedDir)) detected.push('zed');
|
|
5564
5644
|
|
|
5565
5645
|
const editorLabel = (e: string) =>
|
|
5566
|
-
({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
|
|
5646
|
+
({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
|
|
5567
5647
|
|
|
5568
5648
|
console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
|
|
5569
5649
|
const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
|
|
@@ -5603,6 +5683,92 @@ if (process.argv[2] === 'setup') {
|
|
|
5603
5683
|
rl.close();
|
|
5604
5684
|
console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
5605
5685
|
console.log(` Your AI now has persistent memory across every session.\n`);
|
|
5686
|
+
|
|
5687
|
+
// ── Step 7: Show first session_start preview (Aha moment) ────────────────
|
|
5688
|
+
// Fetch brain health from the API to show the user what their agent will see.
|
|
5689
|
+
process.stdout.write('\n⏳ Fetching your Brain health preview...');
|
|
5690
|
+
try {
|
|
5691
|
+
const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
|
|
5692
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
5693
|
+
signal: AbortSignal.timeout(6000),
|
|
5694
|
+
});
|
|
5695
|
+
if (brainRes.ok) {
|
|
5696
|
+
const brainData = await brainRes.json() as {
|
|
5697
|
+
lesson_count?: number; context_count?: number;
|
|
5698
|
+
open_failures?: number; quality_score?: number;
|
|
5699
|
+
};
|
|
5700
|
+
const lessons = brainData.lesson_count ?? 0;
|
|
5701
|
+
const contexts = brainData.context_count ?? 0;
|
|
5702
|
+
const score = brainData.quality_score ?? 0;
|
|
5703
|
+
const level = lessons === 0 ? 'Intern 🌱' :
|
|
5704
|
+
lessons < 10 ? 'Junior Dev 🔧' :
|
|
5705
|
+
lessons < 30 ? 'Mid Dev ⚡' :
|
|
5706
|
+
lessons < 60 ? 'Senior Dev 🧠' :
|
|
5707
|
+
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
5708
|
+
console.log(' ✓\n');
|
|
5709
|
+
console.log('┌──────────────────────────────────────────────────────┐');
|
|
5710
|
+
console.log(`│ 🧠 Brain Health Report │`);
|
|
5711
|
+
console.log('├──────────────────────────────────────────────────────┤');
|
|
5712
|
+
console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
|
|
5713
|
+
console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
|
|
5714
|
+
if (lessons === 0) {
|
|
5715
|
+
console.log('├──────────────────────────────────────────────────────┤');
|
|
5716
|
+
console.log('│ Your Brain is empty and ready to learn. │');
|
|
5717
|
+
console.log('│ After your first coding session it will contain: │');
|
|
5718
|
+
console.log('│ • Lessons from every bug you fixed │');
|
|
5719
|
+
console.log('│ • Your project\'s indexed files │');
|
|
5720
|
+
console.log('│ • A session summary for next time │');
|
|
5721
|
+
}
|
|
5722
|
+
console.log('└──────────────────────────────────────────────────────┘');
|
|
5723
|
+
} else {
|
|
5724
|
+
console.log(' (skipped)');
|
|
5725
|
+
}
|
|
5726
|
+
} catch { console.log(' (skipped)'); }
|
|
5727
|
+
|
|
5728
|
+
console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
|
|
5729
|
+
console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
|
|
5730
|
+
process.exit(0);
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5733
|
+
// ── CLI: cachly index <dir> ───────────────────────────────────────────────────
|
|
5734
|
+
// Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
|
|
5735
|
+
// Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
|
|
5736
|
+
|
|
5737
|
+
if (process.argv[2] === 'index') {
|
|
5738
|
+
const { resolve } = await import('node:path');
|
|
5739
|
+
const argv = process.argv.slice(3);
|
|
5740
|
+
const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
5741
|
+
|
|
5742
|
+
const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
5743
|
+
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
5744
|
+
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
5745
|
+
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
5746
|
+
|
|
5747
|
+
if (!instanceId || !JWT) {
|
|
5748
|
+
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
5749
|
+
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
5750
|
+
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
5751
|
+
console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
|
|
5752
|
+
process.exit(1);
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5755
|
+
console.log(`\n📂 Indexing: ${dir}`);
|
|
5756
|
+
console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
|
|
5757
|
+
|
|
5758
|
+
try {
|
|
5759
|
+
const result = await handleTool('index_project', {
|
|
5760
|
+
instance_id: instanceId,
|
|
5761
|
+
dir,
|
|
5762
|
+
max_files: maxFiles,
|
|
5763
|
+
ttl: 86400 * 7,
|
|
5764
|
+
namespace,
|
|
5765
|
+
});
|
|
5766
|
+
console.log(result);
|
|
5767
|
+
console.log('\n✅ Indexing complete.\n');
|
|
5768
|
+
} catch (err) {
|
|
5769
|
+
console.error(`\n❌ Indexing failed: ${(err as Error).message}\n`);
|
|
5770
|
+
process.exit(1);
|
|
5771
|
+
}
|
|
5606
5772
|
process.exit(0);
|
|
5607
5773
|
}
|
|
5608
5774
|
|
|
@@ -5655,6 +5821,34 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
5655
5821
|
})();
|
|
5656
5822
|
}
|
|
5657
5823
|
|
|
5658
|
-
const
|
|
5659
|
-
|
|
5824
|
+
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
5825
|
+
|
|
5826
|
+
if (httpPort) {
|
|
5827
|
+
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
5828
|
+
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
5829
|
+
const { createServer } = await import('node:http');
|
|
5830
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
5831
|
+
await server.connect(transport);
|
|
5832
|
+
const httpServer = createServer((req, res) => {
|
|
5833
|
+
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
5834
|
+
if (url.pathname === '/health') {
|
|
5835
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5836
|
+
res.end(JSON.stringify({ status: 'ok', version: CURRENT_VERSION }));
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
5839
|
+
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
5840
|
+
transport.handleRequest(req, res);
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
res.writeHead(404);
|
|
5844
|
+
res.end('not found');
|
|
5845
|
+
});
|
|
5846
|
+
httpServer.listen(httpPort, () => {
|
|
5847
|
+
process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
|
|
5848
|
+
});
|
|
5849
|
+
} else {
|
|
5850
|
+
// ── stdio mode (default for local editor use) ───────────────────────────
|
|
5851
|
+
const transport = new StdioServerTransport();
|
|
5852
|
+
await server.connect(transport);
|
|
5853
|
+
}
|
|
5660
5854
|
|