semem
Version:
Semantic Memory for Intelligent Agents
414 lines (370 loc) • 15.5 kB
JavaScript
// MCP Server implementation for Semem
import LLMHandler from '../handlers/LLMHandler.js';
import EmbeddingHandler from '../handlers/EmbeddingHandler.js';
import CacheManager from '../handlers/CacheManager.js';
import SPARQLHelpers from '../utils/SPARQLHelpers.js';
import SearchService from '../services/search/SearchService.js';
import EmbeddingService from '../services/embeddings/EmbeddingService.js';
import SPARQLService from '../services/embeddings/SPARQLService.js';
import { augmentWithAttributes } from '../ragno/augmentWithAttributes.js';
import { aggregateCommunities } from '../ragno/aggregateCommunities.js';
import Config from '../Config.js';
import OllamaConnector from '../connectors/OllamaConnector.js';
import ClaudeConnector from '../connectors/ClaudeConnector.js';
import MistralConnector from '../connectors/MistralConnector.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// MCP Protocol Version
const LATEST_PROTOCOL_VERSION = '2025.1.0';
// Load MCP JSON Schema
const schemaPath = path.resolve('src/types/mcp-schema.json');
const schema = JSON.parse(await fs.readFile(schemaPath, 'utf-8'));
const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(schema);
// Load configuration
const configPath = path.join(process.cwd(), 'config', 'config.json');
console.log('Loading config from:', configPath);
const config = new Config(configPath);
await config.init();
// Initialize cache manager
const cacheManager = new CacheManager({
maxSize: 1000,
ttl: 3600000 // 1 hour
});
// Load Ragno config
const ragnoConfigPath = path.resolve(__dirname, '../../docs/ragno/ragno-config.json');
const ragnoConfig = JSON.parse(await fs.readFile(ragnoConfigPath, 'utf-8'));
// Initialize LLM provider based on config
let llmProvider;
try {
// Get all configured providers and sort by priority (lower number = higher priority)
const providers = (config.get('llmProviders') || []).sort((a, b) => (a.priority || 999) - (b.priority || 999));
console.log('Configured LLM providers:', JSON.stringify(providers, null, 2));
console.log('Environment variables:', {
CLAUDE_API_KEY: process.env.CLAUDE_API_KEY ? '***' : 'Not set',
OLLAMA_API_KEY: process.env.OLLAMA_API_KEY ? '***' : 'Not set',
MISTRAL_API_KEY: process.env.MISTRAL_API_KEY ? '***' : 'Not set'
});
// Try to initialize each provider in order until one succeeds
for (const providerConfig of providers) {
try {
let connector;
switch (providerConfig.type) {
case 'ollama':
console.log(`Initializing Ollama provider with model: ${providerConfig.chatModel}`);
const { baseUrl = 'http://localhost:11434', chatModel = 'qwen2:1.5b' } = providerConfig;
connector = new OllamaConnector({
baseUrl,
chatModel,
embeddingModel: providerConfig.embeddingModel
});
await connector.initialize();
llmProvider = {
type: 'ollama',
generateChat: connector.generateChat.bind(connector),
generateCompletion: connector.generateCompletion.bind(connector),
generateEmbedding: connector.generateEmbedding.bind(connector)
};
console.log('Ollama provider initialized successfully');
break;
case 'claude':
console.log(`Initializing Claude provider with model: ${providerConfig.chatModel}`);
if (!providerConfig.apiKey) {
console.warn('Skipping Claude provider: Missing API key');
continue;
}
connector = new ClaudeConnector(
providerConfig.apiKey,
providerConfig.chatModel || 'claude-3-opus-20240229'
);
await connector.initialize();
llmProvider = {
type: 'claude',
generateChat: connector.generateChat.bind(connector),
generateCompletion: connector.generateCompletion.bind(connector),
generateEmbedding: connector.generateEmbedding.bind(connector)
};
console.log('Claude provider initialized successfully');
break;
case 'mistral':
console.log(`Initializing Mistral provider with model: ${providerConfig.chatModel}`);
if (!providerConfig.apiKey) {
console.warn('Skipping Mistral provider: Missing API key');
continue;
}
connector = new MistralConnector(
providerConfig.apiKey,
providerConfig.baseUrl || 'https://api.mistral.ai/v1',
providerConfig.chatModel || 'mistral-medium'
);
await connector.initialize();
llmProvider = {
type: 'mistral',
generateChat: connector.generateChat.bind(connector),
generateCompletion: connector.generateCompletion.bind(connector),
generateEmbedding: connector.generateEmbedding ? connector.generateEmbedding.bind(connector) : null
};
console.log('Mistral provider initialized successfully');
break;
default:
console.warn(`Unsupported provider type: ${providerConfig.type}`);
continue;
}
// If we successfully initialized a provider, break the loop
if (llmProvider) break;
} catch (error) {
console.error(`Failed to initialize ${providerConfig.type} provider:`, error);
continue;
}
}
// If no provider was successfully initialized, use a mock provider
if (!llmProvider) {
console.warn('No valid LLM provider found, using mock provider');
llmProvider = {
type: 'mock',
generateChat: async () => 'Mock response: No LLM provider configured',
generateCompletion: async () => 'Mock response: No LLM provider configured',
generateEmbedding: async () => Array(1536).fill(0)
};
}
} catch (error) {
console.error('Failed to initialize LLM provider:', error);
process.exit(1);
}
// Initialize handlers
const llmHandler = new LLMHandler(
llmProvider,
config.get('chatModel') || 'qwen2:1.5b',
config.get('llm.temperature') || 0.7
);
const embeddingHandler = new EmbeddingHandler(
llmProvider,
config.get('embeddingModel') || 'nomic-embed-text',
ragnoConfig.ragno?.enrichment?.embedding?.dimensions || 768,
cacheManager
);
// Initialize services
const sparqlConfig = config.get('sparqlEndpoints')?.[0] || {};
const sparqlService = new SPARQLService({
queryEndpoint: process.env.SPARQL_QUERY_ENDPOINT || sparqlConfig.query,
updateEndpoint: process.env.SPARQL_UPDATE_ENDPOINT || sparqlConfig.update,
graphName: config.get('graphName') || 'http://danny.ayers.name/content',
auth: {
user: process.env.SPARQL_USER || sparqlConfig.user,
password: process.env.SPARQL_PASSWORD || sparqlConfig.password
}
});
const embeddingService = new EmbeddingService({
model: config.get('embeddingModel'),
dimension: ragnoConfig.ragno?.enrichment?.embedding?.dimensions || 768
});
const searchService = new SearchService({
embeddingService,
sparqlService,
graphName: config.get('graphName') || 'http://danny.ayers.name/content',
dimension: ragnoConfig.ragno?.enrichment?.embedding?.dimensions || 768
});
const PORT = process.env.MCP_PORT || 4100;
function send(res, status, obj) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(obj));
}
async function handleJSONRPC(req, res, body) {
let msg;
try {
msg = JSON.parse(body);
} catch (e) {
return send(res, 400, { error: 'Invalid JSON' });
}
if (!validate(msg)) {
return send(res, 400, { error: 'Schema validation failed', details: validate.errors });
}
// Resource registry: map resource IDs to files
// MCP resource registry: advertise all available Ragno/Semem facilities
const resources = {
// Core documentation ... (as before)
// ...
SemanticUnit_js: {
id: 'SemanticUnit_js', type: 'code', path: 'src/ragno/SemanticUnit.js', title: 'JS: SemanticUnit Model'
},
// Live services
callLLM: {
id: 'callLLM', type: 'service', title: 'Call LLMHandler',
description: 'Call LLM for completions, summaries, or concept extraction. Params: {prompt, context, systemPrompt}'
},
embedText: {
id: 'embedText', type: 'service', title: 'Generate Embedding',
description: 'Generate embedding for text using EmbeddingHandler. Params: {text}'
},
sparqlQuery: {
id: 'sparqlQuery', type: 'service', title: 'SPARQL SELECT Query',
description: 'Run SPARQL SELECT query via SPARQLHelpers. Params: {endpoint, query, auth}'
},
sparqlUpdate: {
id: 'sparqlUpdate', type: 'service', title: 'SPARQL UPDATE',
description: 'Run SPARQL UPDATE via SPARQLHelpers. Params: {endpoint, query, auth}'
},
searchGraph: {
id: 'searchGraph', type: 'service', title: 'Semantic Search',
description: 'Semantic search using embeddings and Faiss. Params: {queryText, limit}'
},
augmentGraph: {
id: 'augmentGraph', type: 'service', title: 'Graph Attribute Augmentation',
description: 'Augment a graph with LLM-generated attribute summaries. Params: {graph, options}'
},
discoverCommunities: {
id: 'discoverCommunities', type: 'service', title: 'Community Detection',
description: 'Detect and summarize communities in a graph. Params: {graph, options}'
}
};
// List all available resource metadata
if (msg.method === 'listResources') {
const result = Object.values(resources).map(r => ({
id: r.id,
type: r.type,
title: r.title,
description: r.description || undefined
}));
// Validate outgoing response (MCP schema expects JSON-RPC response)
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
}
// Read a specific resource by id
if (msg.method === 'readResource') {
const { id } = msg.params || {};
const resource = resources[id];
if (!resource) {
return send(res, 404, { error: 'Resource not found', id: msg.id });
}
try {
const content = await fs.readFile(resource.path, 'utf-8');
const result = {
id: resource.id,
type: resource.type,
title: resource.title,
content
};
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'Failed to read resource', details: e.message });
}
}
// Live LLM service endpoint
if (msg.method === 'callLLM') {
const { prompt, context, systemPrompt } = msg.params || {};
try {
const result = await llmHandler.generateResponse(prompt, context, systemPrompt);
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'LLM call failed', details: e.message });
}
}
// Live embedding service endpoint
if (msg.method === 'embedText') {
const { text } = msg.params || {};
try {
const result = await embeddingHandler.generateEmbedding(text);
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'Embedding call failed', details: e.message });
}
}
// SPARQL SELECT query endpoint
if (msg.method === 'sparqlQuery') {
const { endpoint, query, auth } = msg.params || {};
try {
const result = await SPARQLHelpers.executeSPARQLQuery(endpoint, query, auth);
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'SPARQL query failed', details: e.message });
}
}
// SPARQL UPDATE endpoint
if (msg.method === 'sparqlUpdate') {
const { endpoint, query, auth } = msg.params || {};
try {
const result = await SPARQLHelpers.executeSPARQLUpdate(endpoint, query, auth);
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'SPARQL update failed', details: e.message });
}
}
// searchGraph endpoint (live semantic search)
if (msg.method === 'searchGraph') {
const { queryText, limit } = msg.params || {};
try {
const results = await searchService.search(queryText, limit || 5);
const response = { jsonrpc: '2.0', id: msg.id, result: results };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'Search call failed', details: e.message });
}
}
// augmentGraph endpoint
if (msg.method === 'augmentGraph') {
const { graph, options } = msg.params || {};
try {
const result = await augmentWithAttributes(graph, llmHandler, options || {});
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'Graph augmentation failed', details: e.message });
}
}
// discoverCommunities endpoint
if (msg.method === 'discoverCommunities') {
const { graph, options } = msg.params || {};
try {
const result = await aggregateCommunities(graph, llmHandler, options || {});
const response = { jsonrpc: '2.0', id: msg.id, result };
if (!validate(response)) {
return send(res, 500, { error: 'Outgoing schema validation failed', details: validate.errors });
}
return send(res, 200, response);
} catch (e) {
return send(res, 500, { error: 'Community detection failed', details: e.message });
}
}
// ... add more methods as needed
send(res, 404, { error: 'Method not found', id: msg.id });
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST') {
send(res, 405, { error: 'Only POST supported' });
return;
}
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => handleJSONRPC(req, res, body));
});
server.listen(PORT, () => {
console.log(`MCP server running on http://localhost:${PORT}/ (protocol ${LATEST_PROTOCOL_VERSION})`);
});