@nxuss/lemma 1.2.2 → 1.2.3

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.
Files changed (2) hide show
  1. package/mcp-server.js +125 -194
  2. package/package.json +2 -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
- const BRAIN_FILE = path.join(os.homedir(), '.lemma-cache', 'lemma_brain.json');
9
- const INPUT_COST = 3.00 / 1000000;
10
- const OUTPUT_COST = 15.00 / 1000000;
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
- function loadBrain() {
13
- try {
14
- if (!fs.existsSync(BRAIN_FILE)) {
15
- fs.mkdirSync(path.dirname(BRAIN_FILE), { recursive: true });
16
- fs.writeFileSync(BRAIN_FILE, '[]', 'utf8');
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 idf(corpusTokens) {
42
- const df = {};
43
- const N = corpusTokens.length;
44
- for (const tokens of corpusTokens) {
45
- const seen = new Set(tokens);
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 bm25(queryTokens, docTokens, idfScores, avgDocLen) {
56
- const k1 = 1.5;
57
- const b = 0.75;
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
- const tools = [
94
- {
95
- name: 'get_cached_solution',
96
- description: 'Search the semantic cache (BM25) for a similar prompt. Hit >= 85% returns cached response.',
97
- inputSchema: {
98
- type: 'object',
99
- properties: {
100
- prompt: { type: 'string', description: 'The prompt to search for' },
101
- threshold: { type: 'number', description: 'Similarity threshold 0-1', default: 0.85 }
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 toolHandlers = {
121
- get_cached_solution: async (args) => {
122
- const prompt = args.prompt;
123
- const threshold = args.threshold || 0.85;
124
- if (!prompt) throw new Error('prompt is required');
125
- const brain = loadBrain();
126
- const results = search(brain, prompt, threshold);
127
- if (results.length > 0) {
128
- const hit = results[0];
129
- brain[hit.index].hits = (brain[hit.index].hits || 0) + 1;
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
- return { content: [{ type: 'text', text: JSON.stringify({ hit: false }) }] };
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
- process.stdin.on('data', async (chunk) => {
161
- const lines = chunk.toString().split('\n').filter(l => l.trim());
162
- for (const line of lines) {
163
- try {
164
- const msg = JSON.parse(line);
165
- if (msg.method === 'initialize') {
166
- const resp = {
167
- jsonrpc: '2.0',
168
- id: msg.id,
169
- result: {
170
- protocolVersion: '2024-11-05',
171
- serverInfo: { name: server.name, version: server.version },
172
- capabilities: { tools: {} }
173
- }
174
- };
175
- console.log(JSON.stringify(resp));
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
- try {
194
- const result = await handler(msg.params.arguments || {});
195
- console.log(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result }));
196
- } catch (e) {
197
- console.log(JSON.stringify({
198
- jsonrpc: '2.0',
199
- id: msg.id,
200
- error: { code: -32000, message: e.message }
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
- } catch (e) {
212
- console.error('[lemma-mcp] Parse error:', e.message);
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('SIGINT', () => process.exit(0));
218
- process.on('SIGTERM', () => process.exit(0));
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
- console.error('[lemma-mcp] BM25 cache server ready on stdio');
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.2",
3
+ "version": "1.2.3",
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,7 @@
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",
173
+ "@nxuss/lemma": "^1.2.3",
174
174
  "axios": "^1.6.0",
175
175
  "commander": "^14.0.3",
176
176
  "cors": "^2.8.6",