@nxuss/lemma 1.2.2 → 1.2.4
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/bin/cli.js +14 -3
- package/bin/init.js +35 -14
- package/cache-proxy.js +14 -2
- package/dist/cjs/mcp/tools.js +1 -1
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/esm/mcp/tools.js +1 -1
- package/dist/esm/mcp/tools.js.map +1 -1
- package/mcp-server.js +125 -194
- package/package.json +1 -2
package/mcp-server.js
CHANGED
|
@@ -3,218 +3,149 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Lemma MCP Server Entry Point
|
|
9
|
+
*
|
|
10
|
+
* This file routes to the full MCP server implementation.
|
|
11
|
+
* The full server with 70+ tools is in dist/cjs/mcp/index.js
|
|
12
|
+
*/
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const fullMcpPath = path.join(__dirname, 'dist', 'cjs', 'mcp', 'index.js');
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(fullMcpPath)) {
|
|
17
|
+
// Load the full MCP server with all tools
|
|
18
|
+
require(fullMcpPath);
|
|
19
|
+
} else {
|
|
20
|
+
console.error('[lemma-mcp] ERROR: Full MCP server not found at:', fullMcpPath);
|
|
21
|
+
console.error('[lemma-mcp] Please run "npm run build" to compile the project.');
|
|
22
|
+
console.error('[lemma-mcp] Falling back to legacy minimal server (only 2 tools)...');
|
|
23
|
+
|
|
24
|
+
// Legacy fallback - minimal BM25 cache server
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const BRAIN_FILE = path.join(os.homedir(), '.lemma-cache', 'lemma_brain.json');
|
|
27
|
+
|
|
28
|
+
function loadBrain() {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(BRAIN_FILE)) {
|
|
31
|
+
fs.mkdirSync(path.dirname(BRAIN_FILE), { recursive: true });
|
|
32
|
+
fs.writeFileSync(BRAIN_FILE, '[]', 'utf8');
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return JSON.parse(fs.readFileSync(BRAIN_FILE, 'utf8'));
|
|
36
|
+
} catch (e) {
|
|
17
37
|
return [];
|
|
18
38
|
}
|
|
19
|
-
const raw = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
20
|
-
return JSON.parse(raw);
|
|
21
|
-
} catch (e) {
|
|
22
|
-
console.error('[lemma-mcp] Failed to load brain, initializing empty:', e.message);
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function saveBrain(data) {
|
|
28
|
-
try {
|
|
29
|
-
fs.mkdirSync(path.dirname(BRAIN_FILE), { recursive: true });
|
|
30
|
-
fs.writeFileSync(BRAIN_FILE, JSON.stringify(data), 'utf8');
|
|
31
|
-
} catch (e) {
|
|
32
|
-
console.error('[lemma-mcp] Failed to save brain:', e.message);
|
|
33
39
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function tokenize(text) {
|
|
37
|
-
const stopWords = new Set(['the','a','an','is','are','was','were','be','been','being','have','has','had','do','does','did','will','would','could','should','may','might','shall','can','need','dare','ought','used','to','of','in','for','on','with','at','by','from','as','into','through','during','before','after','above','below','between','out','off','over','under','again','further','then','once','here','there','when','where','why','how','all','each','every','both','few','more','most','other','some','such','no','nor','not','only','own','same','so','than','too','very','just','because','but','and','or','if','while','although','since','until','unless','about','up','it','its','this','that','these','those','i','me','my','myself','we','our','ours','ourselves','you','your','yours','yourself','yourselves','he','him','his','himself','she','her','hers','herself','they','them','their','theirs','themselves','what','which','who','whom','this']);
|
|
38
|
-
return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(w => w.length > 1 && !stopWords.has(w));
|
|
39
|
-
}
|
|
40
40
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (const t of seen) df[t] = (df[t] || 0) + 1;
|
|
47
|
-
}
|
|
48
|
-
const scores = {};
|
|
49
|
-
for (const [t, count] of Object.entries(df)) {
|
|
50
|
-
scores[t] = Math.log(1 + (N - count + 0.5) / (count + 0.5));
|
|
41
|
+
function saveBrain(data) {
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(path.dirname(BRAIN_FILE), { recursive: true });
|
|
44
|
+
fs.writeFileSync(BRAIN_FILE, JSON.stringify(data), 'utf8');
|
|
45
|
+
} catch (e) {}
|
|
51
46
|
}
|
|
52
|
-
return scores;
|
|
53
|
-
}
|
|
54
47
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let score = 0;
|
|
59
|
-
const docLen = docTokens.length;
|
|
60
|
-
const tf = {};
|
|
61
|
-
for (const t of docTokens) tf[t] = (tf[t] || 0) + 1;
|
|
62
|
-
for (const q of queryTokens) {
|
|
63
|
-
const f = tf[q] || 0;
|
|
64
|
-
const idfVal = idfScores[q] || 0;
|
|
65
|
-
if (f === 0) continue;
|
|
66
|
-
score += idfVal * ((f * (k1 + 1)) / (f + k1 * (1 - b + b * (docLen / avgDocLen))));
|
|
48
|
+
function tokenize(text) {
|
|
49
|
+
const stopWords = new Set(['the','a','an','is','are','was','were','be','been','being','have','has','had','do','does','did','will','would','could','should','may','might','shall','can','need','dare','ought','used','to','of','in','for','on','with','at','by','from','as','into','through','during','before','after','above','below','between','out','off','over','under','again','further','then','once','here','there','when','where','why','how','all','each','every','both','few','more','most','other','some','such','no','nor','not','only','own','same','so','than','too','very','just','because','but','and','or','if','while','although','since','until','unless','about','up','it','its','this','that','these','those','i','me','my','myself','we','our','ours','ourselves','you','your','yours','yourself','yourselves','he','him','his','himself','she','her','hers','herself','they','them','their','theirs','themselves','what','which','who','whom','this']);
|
|
50
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(w => w.length > 1 && !stopWords.has(w));
|
|
67
51
|
}
|
|
68
|
-
return score;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function buildIndex(entries) {
|
|
72
|
-
if (!entries || entries.length === 0) return { corpusTokens: [], idfScores: {}, avgDocLen: 0 };
|
|
73
|
-
const corpusTokens = entries.map(e => tokenize(e.prompt || ''));
|
|
74
|
-
const avgDocLen = corpusTokens.reduce((s, t) => s + t.length, 0) / corpusTokens.length;
|
|
75
|
-
const idfScores = idf(corpusTokens);
|
|
76
|
-
return { corpusTokens, idfScores, avgDocLen };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function search(entries, query, threshold) {
|
|
80
|
-
if (!entries || entries.length === 0) return [];
|
|
81
|
-
const qt = tokenize(query);
|
|
82
|
-
if (qt.length === 0) return [];
|
|
83
|
-
const idx = buildIndex(entries);
|
|
84
|
-
const maxScore = bm25(qt, qt, idx.idfScores, idx.avgDocLen);
|
|
85
|
-
const results = entries.map((entry, i) => {
|
|
86
|
-
const raw = bm25(qt, idx.corpusTokens[i], idx.idfScores, idx.avgDocLen);
|
|
87
|
-
const similarity = maxScore > 0 ? raw / maxScore : 0;
|
|
88
|
-
return { entry, similarity, index: i };
|
|
89
|
-
}).filter(r => r.similarity >= threshold).sort((a, b) => b.similarity - a.similarity);
|
|
90
|
-
return results;
|
|
91
|
-
}
|
|
92
52
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
required: ['prompt']
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: 'store_cached_solution',
|
|
108
|
-
description: 'Store a prompt-response pair in the semantic cache for future lookups.',
|
|
109
|
-
inputSchema: {
|
|
110
|
-
type: 'object',
|
|
111
|
-
properties: {
|
|
112
|
-
prompt: { type: 'string', description: 'The prompt' },
|
|
113
|
-
response: { type: 'string', description: 'The response' }
|
|
114
|
-
},
|
|
115
|
-
required: ['prompt', 'response']
|
|
53
|
+
function bm25(queryTokens, docTokens, idfScores, avgDocLen) {
|
|
54
|
+
const k1 = 1.5, b = 0.75;
|
|
55
|
+
let score = 0;
|
|
56
|
+
const docLen = docTokens.length;
|
|
57
|
+
const tf = {};
|
|
58
|
+
for (const t of docTokens) tf[t] = (tf[t] || 0) + 1;
|
|
59
|
+
for (const q of queryTokens) {
|
|
60
|
+
const f = tf[q] || 0;
|
|
61
|
+
if (f === 0) continue;
|
|
62
|
+
score += (idfScores[q] || 0) * ((f * (k1 + 1)) / (f + k1 * (1 - b + b * (docLen / avgDocLen))));
|
|
116
63
|
}
|
|
64
|
+
return score;
|
|
117
65
|
}
|
|
118
|
-
];
|
|
119
66
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
saveBrain(brain);
|
|
131
|
-
return {
|
|
132
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
133
|
-
hit: true,
|
|
134
|
-
similarity: hit.similarity,
|
|
135
|
-
response: hit.entry.response,
|
|
136
|
-
hits: brain[hit.index].hits
|
|
137
|
-
}) }]
|
|
138
|
-
};
|
|
67
|
+
const tools = [
|
|
68
|
+
{
|
|
69
|
+
name: 'get_cached_solution',
|
|
70
|
+
description: 'Search the semantic cache (BM25) for a similar prompt.',
|
|
71
|
+
inputSchema: { type: 'object', properties: { prompt: { type: 'string' }, threshold: { type: 'number', default: 0.85 } }, required: ['prompt'] }
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'store_cached_solution',
|
|
75
|
+
description: 'Store a prompt-response pair in the semantic cache.',
|
|
76
|
+
inputSchema: { type: 'object', properties: { prompt: { type: 'string' }, response: { type: 'string' } }, required: ['prompt', 'response'] }
|
|
139
77
|
}
|
|
140
|
-
|
|
141
|
-
},
|
|
142
|
-
store_cached_solution: async (args) => {
|
|
143
|
-
const prompt = args.prompt;
|
|
144
|
-
const response = args.response;
|
|
145
|
-
if (!prompt || !response) throw new Error('prompt and response are required');
|
|
146
|
-
const brain = loadBrain();
|
|
147
|
-
brain.push({ prompt, response, hits: 0, createdAt: Date.now() });
|
|
148
|
-
saveBrain(brain);
|
|
149
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, total: brain.length }) }] };
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const server = {
|
|
154
|
-
name: 'lemma-mcp-server',
|
|
155
|
-
version: '1.0.0',
|
|
156
|
-
tools,
|
|
157
|
-
toolHandlers
|
|
158
|
-
};
|
|
78
|
+
];
|
|
159
79
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} else if (msg.method === 'tools/list') {
|
|
177
|
-
const resp = {
|
|
178
|
-
jsonrpc: '2.0',
|
|
179
|
-
id: msg.id,
|
|
180
|
-
result: { tools: server.tools }
|
|
181
|
-
};
|
|
182
|
-
console.log(JSON.stringify(resp));
|
|
183
|
-
} else if (msg.method === 'tools/call') {
|
|
184
|
-
const handler = server.toolHandlers[msg.params.name];
|
|
185
|
-
if (!handler) {
|
|
186
|
-
console.log(JSON.stringify({
|
|
187
|
-
jsonrpc: '2.0',
|
|
188
|
-
id: msg.id,
|
|
189
|
-
error: { code: -32601, message: `Tool not found: ${msg.params.name}` }
|
|
190
|
-
}));
|
|
191
|
-
continue;
|
|
80
|
+
const handlers = {
|
|
81
|
+
get_cached_solution: async (args) => {
|
|
82
|
+
const brain = loadBrain();
|
|
83
|
+
const qt = tokenize(args.prompt);
|
|
84
|
+
if (qt.length === 0) return { content: [{ type: 'text', text: JSON.stringify({ hit: false }) }] };
|
|
85
|
+
|
|
86
|
+
let bestMatch = null;
|
|
87
|
+
let bestScore = 0;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < brain.length; i++) {
|
|
90
|
+
const docTokens = tokenize(brain[i].prompt || '');
|
|
91
|
+
const corpusTokens = brain.map(e => tokenize(e.prompt || ''));
|
|
92
|
+
const df = {};
|
|
93
|
+
for (const tokens of corpusTokens) {
|
|
94
|
+
const seen = new Set(tokens);
|
|
95
|
+
for (const t of seen) df[t] = (df[t] || 0) + 1;
|
|
192
96
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
97
|
+
const idfScores = {};
|
|
98
|
+
for (const [t, count] of Object.entries(df)) {
|
|
99
|
+
idfScores[t] = Math.log(1 + (corpusTokens.length - count + 0.5) / (count + 0.5));
|
|
100
|
+
}
|
|
101
|
+
const avgDocLen = corpusTokens.reduce((s, t) => s + t.length, 0) / corpusTokens.length;
|
|
102
|
+
const score = bm25(qt, docTokens, idfScores, avgDocLen);
|
|
103
|
+
const maxScore = bm25(qt, qt, idfScores, avgDocLen);
|
|
104
|
+
const similarity = maxScore > 0 ? score / maxScore : 0;
|
|
105
|
+
|
|
106
|
+
if (similarity >= (args.threshold || 0.85) && similarity > bestScore) {
|
|
107
|
+
bestScore = similarity;
|
|
108
|
+
bestMatch = { entry: brain[i], similarity, index: i };
|
|
202
109
|
}
|
|
203
|
-
} else if (msg.method === 'notifications/initialized') {
|
|
204
|
-
} else if (msg.id) {
|
|
205
|
-
console.log(JSON.stringify({
|
|
206
|
-
jsonrpc: '2.0',
|
|
207
|
-
id: msg.id,
|
|
208
|
-
error: { code: -32601, message: `Method not found: ${msg.method}` }
|
|
209
|
-
}));
|
|
210
110
|
}
|
|
211
|
-
|
|
212
|
-
|
|
111
|
+
|
|
112
|
+
if (bestMatch) {
|
|
113
|
+
brain[bestMatch.index].hits = (brain[bestMatch.index].hits || 0) + 1;
|
|
114
|
+
saveBrain(brain);
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify({ hit: true, similarity: bestMatch.similarity, response: bestMatch.entry.response }) }] };
|
|
116
|
+
}
|
|
117
|
+
return { content: [{ type: 'text', text: JSON.stringify({ hit: false }) }] };
|
|
118
|
+
},
|
|
119
|
+
store_cached_solution: async (args) => {
|
|
120
|
+
const brain = loadBrain();
|
|
121
|
+
brain.push({ prompt: args.prompt, response: args.response, hits: 0, createdAt: Date.now() });
|
|
122
|
+
saveBrain(brain);
|
|
123
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, total: brain.length }) }] };
|
|
213
124
|
}
|
|
214
|
-
}
|
|
215
|
-
});
|
|
125
|
+
};
|
|
216
126
|
|
|
217
|
-
process.on('
|
|
218
|
-
|
|
127
|
+
process.stdin.on('data', async (chunk) => {
|
|
128
|
+
for (const line of chunk.toString().split('\n').filter(l => l.trim())) {
|
|
129
|
+
try {
|
|
130
|
+
const msg = JSON.parse(line);
|
|
131
|
+
if (msg.method === 'initialize') {
|
|
132
|
+
console.log(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { protocolVersion: '2024-11-05', serverInfo: { name: 'lemma-mcp-server', version: '1.0.0' }, capabilities: { tools: {} } } }));
|
|
133
|
+
} else if (msg.method === 'tools/list') {
|
|
134
|
+
console.log(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { tools } }));
|
|
135
|
+
} else if (msg.method === 'tools/call') {
|
|
136
|
+
const handler = handlers[msg.params.name];
|
|
137
|
+
if (!handler) {
|
|
138
|
+
console.log(JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Tool not found: ${msg.params.name}` } }));
|
|
139
|
+
} else {
|
|
140
|
+
const result = await handler(msg.params.arguments || {});
|
|
141
|
+
console.log(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
219
147
|
|
|
220
|
-
|
|
148
|
+
process.on('SIGINT', () => process.exit(0));
|
|
149
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
150
|
+
console.error('[lemma-mcp] Legacy fallback server ready (2 tools only)');
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxuss/lemma",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Intelligent AI Gateway for IDEs & Agents — Semantic cache, Privacy Firewall, Infrastructure Command Center, and Autonomous Cost-Optimization.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -170,7 +170,6 @@
|
|
|
170
170
|
"dependencies": {
|
|
171
171
|
"@chroma-core/default-embed": "^0.1.9",
|
|
172
172
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
173
|
-
"@nxuss/lemma": "^1.1.1",
|
|
174
173
|
"axios": "^1.6.0",
|
|
175
174
|
"commander": "^14.0.3",
|
|
176
175
|
"cors": "^2.8.6",
|