@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/Dockerfile
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
FROM node:22-alpine AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY package*.json tsconfig*.json ./
|
|
4
|
+
RUN npm ci --ignore-scripts
|
|
5
|
+
COPY src/ ./src/
|
|
6
|
+
RUN npm run build
|
|
7
|
+
|
|
1
8
|
FROM node:22-alpine
|
|
2
9
|
WORKDIR /app
|
|
3
10
|
COPY package*.json ./
|
|
4
|
-
RUN npm ci --omit=dev
|
|
5
|
-
COPY dist/ ./dist/
|
|
11
|
+
RUN npm ci --omit=dev --ignore-scripts
|
|
12
|
+
COPY --from=builder /app/dist/ ./dist/
|
|
13
|
+
ENV NODE_ENV=production
|
|
14
|
+
ENV PORT=3000
|
|
6
15
|
EXPOSE 3000
|
|
7
16
|
ENTRYPOINT ["node", "dist/index.js"]
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Stopwords — filtered out during tokenization.
|
|
4
|
-
* Covers: English, German, French, Spanish, Italian, Portuguese
|
|
4
|
+
* Covers: English, German, French, Spanish, Italian, Portuguese,
|
|
5
|
+
* Chinese (Simplified + Traditional), Japanese, Korean.
|
|
5
6
|
* Keeps the index small and scores meaningful.
|
|
6
7
|
*/
|
|
7
8
|
declare const STOPWORDS: Set<string>;
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Tokenize text into meaningful keywords.
|
|
11
|
+
* Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
|
|
12
|
+
* CJK text is split into character bigrams (the standard for indexing logographic languages).
|
|
13
|
+
*/
|
|
9
14
|
declare function tokenize(text: string): string[];
|
|
10
15
|
/**
|
|
11
16
|
* Split a multi-topic query into sub-queries.
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { join, relative, extname } from 'node:path';
|
|
|
40
40
|
*/
|
|
41
41
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
42
42
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
43
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
43
44
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
44
45
|
import { Redis } from 'ioredis';
|
|
45
46
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
@@ -287,12 +288,14 @@ function embedProviderHint() {
|
|
|
287
288
|
// • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
|
|
288
289
|
// • Multi-query splitting — numbered lists, semicolons, conjunctions
|
|
289
290
|
// • Fuzzy matching with Levenshtein distance ≤ 2
|
|
290
|
-
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT)
|
|
291
|
+
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO)
|
|
292
|
+
// • CJK character bigram extraction — handles Chinese, Japanese, Korean
|
|
291
293
|
// • Pipeline Redis reads for performance
|
|
292
294
|
//
|
|
293
295
|
/**
|
|
294
296
|
* Stopwords — filtered out during tokenization.
|
|
295
|
-
* Covers: English, German, French, Spanish, Italian, Portuguese
|
|
297
|
+
* Covers: English, German, French, Spanish, Italian, Portuguese,
|
|
298
|
+
* Chinese (Simplified + Traditional), Japanese, Korean.
|
|
296
299
|
* Keeps the index small and scores meaningful.
|
|
297
300
|
*/
|
|
298
301
|
const STOPWORDS = new Set([
|
|
@@ -366,14 +369,61 @@ const STOPWORDS = new Set([
|
|
|
366
369
|
'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
|
|
367
370
|
'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
|
|
368
371
|
'todo', 'toda', 'todos', 'cada', 'outro', 'outra', 'quando', 'porque',
|
|
372
|
+
// ── Chinese (Simplified + Traditional) — high-frequency function characters ──
|
|
373
|
+
'的', '了', '是', '在', '不', '和', '我', '他', '这', '中', '大', '为',
|
|
374
|
+
'上', '个', '国', '以', '要', '就', '出', '说', '们', '有', '来', '到',
|
|
375
|
+
'时', '地', '年', '得', '着', '那', '过', '后', '还', '与', '也', '可',
|
|
376
|
+
'于', '从', '但', '而', '被', '把', '让', '使', '对', '很', '都', '一',
|
|
377
|
+
'会', '没', '人', '它', '这个', '那个', '什么', '如果', '因为', '所以',
|
|
378
|
+
'已经', '可以', '这些', '那些', '我们', '他们', '她们', '它们',
|
|
379
|
+
// ── Japanese — common hiragana particles and auxiliary verbs ──
|
|
380
|
+
'の', 'は', 'が', 'を', 'に', 'で', 'と', 'も', 'や', 'か', 'な', 'ね',
|
|
381
|
+
'よ', 'わ', 'て', 'い', 'う', 'え', 'お', 'き', 'く', 'け', 'こ', 'さ',
|
|
382
|
+
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'ぬ', 'ん', 'から', 'まで',
|
|
383
|
+
'より', 'へ', 'です', 'ます', 'ない', 'ある', 'いる', 'する', 'こと',
|
|
384
|
+
'もの', 'ので', 'では', 'には', 'との', 'への', 'から', 'まで',
|
|
385
|
+
// ── Korean — common particles and auxiliary forms ──
|
|
386
|
+
'이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
|
|
387
|
+
'로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
|
|
388
|
+
'이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
|
|
369
389
|
]);
|
|
370
|
-
|
|
390
|
+
// CJK Unicode ranges used for bigram extraction
|
|
391
|
+
const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
|
|
392
|
+
const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
|
|
393
|
+
/**
|
|
394
|
+
* Tokenize text into meaningful keywords.
|
|
395
|
+
* Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
|
|
396
|
+
* CJK text is split into character bigrams (the standard for indexing logographic languages).
|
|
397
|
+
*/
|
|
371
398
|
function tokenize(text) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
.
|
|
376
|
-
|
|
399
|
+
const tokens = [];
|
|
400
|
+
const lower = text.toLowerCase();
|
|
401
|
+
for (const [seg] of lower.matchAll(SEGMENT_RE)) {
|
|
402
|
+
if (CJK_RE.test(seg)) {
|
|
403
|
+
// CJK segment — extract overlapping character bigrams
|
|
404
|
+
for (let i = 0; i < seg.length - 1; i++) {
|
|
405
|
+
const bigram = seg[i] + seg[i + 1];
|
|
406
|
+
if (!STOPWORDS.has(seg[i]) && !STOPWORDS.has(bigram)) {
|
|
407
|
+
tokens.push(bigram);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Trailing single character (for 1-char CJK terms)
|
|
411
|
+
if (seg.length >= 1) {
|
|
412
|
+
const last = seg[seg.length - 1];
|
|
413
|
+
if (!STOPWORDS.has(last))
|
|
414
|
+
tokens.push(last);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
// Latin / other — existing logic
|
|
419
|
+
const words = seg
|
|
420
|
+
.replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
|
|
421
|
+
.split(/\s+/)
|
|
422
|
+
.filter(w => w.length > 1 && !STOPWORDS.has(w));
|
|
423
|
+
tokens.push(...words);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return tokens;
|
|
377
427
|
}
|
|
378
428
|
/**
|
|
379
429
|
* Split a multi-topic query into sub-queries.
|
|
@@ -4649,6 +4699,8 @@ const EDITOR_FILES = {
|
|
|
4649
4699
|
windsurf: '.windsurf/mcp.json',
|
|
4650
4700
|
copilot: '.vscode/mcp.json',
|
|
4651
4701
|
continue: '.continue/config.json',
|
|
4702
|
+
cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
|
|
4703
|
+
zed: '.zed/settings.json', // Zed project-level context_servers
|
|
4652
4704
|
};
|
|
4653
4705
|
const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
4654
4706
|
const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
|
|
@@ -4663,6 +4715,20 @@ function buildMcpConfig(apiKey, instanceId, editor) {
|
|
|
4663
4715
|
},
|
|
4664
4716
|
}, null, 2);
|
|
4665
4717
|
}
|
|
4718
|
+
if (editor === 'zed') {
|
|
4719
|
+
return JSON.stringify({
|
|
4720
|
+
context_servers: {
|
|
4721
|
+
cachly: {
|
|
4722
|
+
command: {
|
|
4723
|
+
path: 'npx',
|
|
4724
|
+
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
4725
|
+
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
4726
|
+
},
|
|
4727
|
+
settings: {},
|
|
4728
|
+
},
|
|
4729
|
+
},
|
|
4730
|
+
}, null, 2);
|
|
4731
|
+
}
|
|
4666
4732
|
return JSON.stringify({
|
|
4667
4733
|
mcpServers: {
|
|
4668
4734
|
cachly: {
|
|
@@ -4991,6 +5057,8 @@ if (process.argv[2] === 'setup') {
|
|
|
4991
5057
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
4992
5058
|
const cwd = process.cwd();
|
|
4993
5059
|
const detected = [];
|
|
5060
|
+
const { homedir } = await import('node:os');
|
|
5061
|
+
const home = homedir();
|
|
4994
5062
|
// Claude Code: always include (CLAUDE.md is universal)
|
|
4995
5063
|
detected.push('claude');
|
|
4996
5064
|
if (existsSync(resolve(cwd, '.cursor')))
|
|
@@ -5001,7 +5069,21 @@ if (process.argv[2] === 'setup') {
|
|
|
5001
5069
|
detected.push('copilot');
|
|
5002
5070
|
if (existsSync(resolve(cwd, '.continue')))
|
|
5003
5071
|
detected.push('continue');
|
|
5004
|
-
|
|
5072
|
+
// Cline: VS Code extension — detect via globalStorage on the machine
|
|
5073
|
+
const clineGlobalDir = process.platform === 'darwin'
|
|
5074
|
+
? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
|
|
5075
|
+
: resolve(home, '.vscode/extensions');
|
|
5076
|
+
if (existsSync(clineGlobalDir) && !detected.includes('copilot'))
|
|
5077
|
+
detected.push('cline');
|
|
5078
|
+
else if (existsSync(clineGlobalDir))
|
|
5079
|
+
detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
|
|
5080
|
+
// Zed editor — detect via app data dir
|
|
5081
|
+
const zedDir = process.platform === 'darwin'
|
|
5082
|
+
? resolve(home, 'Library/Application Support/Zed')
|
|
5083
|
+
: resolve(home, '.config/zed');
|
|
5084
|
+
if (existsSync(zedDir))
|
|
5085
|
+
detected.push('zed');
|
|
5086
|
+
const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
|
|
5005
5087
|
console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
|
|
5006
5088
|
const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
|
|
5007
5089
|
const editorsToSetup = !editorsRaw || editorsRaw.toLowerCase() === 'all'
|
|
@@ -5042,6 +5124,86 @@ if (process.argv[2] === 'setup') {
|
|
|
5042
5124
|
rl.close();
|
|
5043
5125
|
console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
5044
5126
|
console.log(` Your AI now has persistent memory across every session.\n`);
|
|
5127
|
+
// ── Step 7: Show first session_start preview (Aha moment) ────────────────
|
|
5128
|
+
// Fetch brain health from the API to show the user what their agent will see.
|
|
5129
|
+
process.stdout.write('\n⏳ Fetching your Brain health preview...');
|
|
5130
|
+
try {
|
|
5131
|
+
const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
|
|
5132
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
5133
|
+
signal: AbortSignal.timeout(6000),
|
|
5134
|
+
});
|
|
5135
|
+
if (brainRes.ok) {
|
|
5136
|
+
const brainData = await brainRes.json();
|
|
5137
|
+
const lessons = brainData.lesson_count ?? 0;
|
|
5138
|
+
const contexts = brainData.context_count ?? 0;
|
|
5139
|
+
const score = brainData.quality_score ?? 0;
|
|
5140
|
+
const level = lessons === 0 ? 'Intern 🌱' :
|
|
5141
|
+
lessons < 10 ? 'Junior Dev 🔧' :
|
|
5142
|
+
lessons < 30 ? 'Mid Dev ⚡' :
|
|
5143
|
+
lessons < 60 ? 'Senior Dev 🧠' :
|
|
5144
|
+
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
5145
|
+
console.log(' ✓\n');
|
|
5146
|
+
console.log('┌──────────────────────────────────────────────────────┐');
|
|
5147
|
+
console.log(`│ 🧠 Brain Health Report │`);
|
|
5148
|
+
console.log('├──────────────────────────────────────────────────────┤');
|
|
5149
|
+
console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
|
|
5150
|
+
console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
|
|
5151
|
+
if (lessons === 0) {
|
|
5152
|
+
console.log('├──────────────────────────────────────────────────────┤');
|
|
5153
|
+
console.log('│ Your Brain is empty and ready to learn. │');
|
|
5154
|
+
console.log('│ After your first coding session it will contain: │');
|
|
5155
|
+
console.log('│ • Lessons from every bug you fixed │');
|
|
5156
|
+
console.log('│ • Your project\'s indexed files │');
|
|
5157
|
+
console.log('│ • A session summary for next time │');
|
|
5158
|
+
}
|
|
5159
|
+
console.log('└──────────────────────────────────────────────────────┘');
|
|
5160
|
+
}
|
|
5161
|
+
else {
|
|
5162
|
+
console.log(' (skipped)');
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
catch {
|
|
5166
|
+
console.log(' (skipped)');
|
|
5167
|
+
}
|
|
5168
|
+
console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
|
|
5169
|
+
console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
|
|
5170
|
+
process.exit(0);
|
|
5171
|
+
}
|
|
5172
|
+
// ── CLI: cachly index <dir> ───────────────────────────────────────────────────
|
|
5173
|
+
// Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
|
|
5174
|
+
// Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
|
|
5175
|
+
if (process.argv[2] === 'index') {
|
|
5176
|
+
const { resolve } = await import('node:path');
|
|
5177
|
+
const argv = process.argv.slice(3);
|
|
5178
|
+
const flag = (name) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
5179
|
+
const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
5180
|
+
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
5181
|
+
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
5182
|
+
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
5183
|
+
if (!instanceId || !JWT) {
|
|
5184
|
+
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
5185
|
+
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
5186
|
+
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
5187
|
+
console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
|
|
5188
|
+
process.exit(1);
|
|
5189
|
+
}
|
|
5190
|
+
console.log(`\n📂 Indexing: ${dir}`);
|
|
5191
|
+
console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
|
|
5192
|
+
try {
|
|
5193
|
+
const result = await handleTool('index_project', {
|
|
5194
|
+
instance_id: instanceId,
|
|
5195
|
+
dir,
|
|
5196
|
+
max_files: maxFiles,
|
|
5197
|
+
ttl: 86400 * 7,
|
|
5198
|
+
namespace,
|
|
5199
|
+
});
|
|
5200
|
+
console.log(result);
|
|
5201
|
+
console.log('\n✅ Indexing complete.\n');
|
|
5202
|
+
}
|
|
5203
|
+
catch (err) {
|
|
5204
|
+
console.error(`\n❌ Indexing failed: ${err.message}\n`);
|
|
5205
|
+
process.exit(1);
|
|
5206
|
+
}
|
|
5045
5207
|
process.exit(0);
|
|
5046
5208
|
}
|
|
5047
5209
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
@@ -5084,6 +5246,34 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
5084
5246
|
catch { /* ignore – network unavailable or timeout */ }
|
|
5085
5247
|
})();
|
|
5086
5248
|
}
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5249
|
+
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
5250
|
+
if (httpPort) {
|
|
5251
|
+
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
5252
|
+
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
5253
|
+
const { createServer } = await import('node:http');
|
|
5254
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
5255
|
+
await server.connect(transport);
|
|
5256
|
+
const httpServer = createServer((req, res) => {
|
|
5257
|
+
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
5258
|
+
if (url.pathname === '/health') {
|
|
5259
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5260
|
+
res.end(JSON.stringify({ status: 'ok', version: CURRENT_VERSION }));
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
5264
|
+
transport.handleRequest(req, res);
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
res.writeHead(404);
|
|
5268
|
+
res.end('not found');
|
|
5269
|
+
});
|
|
5270
|
+
httpServer.listen(httpPort, () => {
|
|
5271
|
+
process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
|
|
5272
|
+
});
|
|
5273
|
+
}
|
|
5274
|
+
else {
|
|
5275
|
+
// ── stdio mode (default for local editor use) ───────────────────────────
|
|
5276
|
+
const transport = new StdioServerTransport();
|
|
5277
|
+
await server.connect(transport);
|
|
5278
|
+
}
|
|
5089
5279
|
//# sourceMappingURL=index.js.map
|