UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

222 lines (187 loc) 6.58 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import http from 'http'; import express from 'express'; import { z } from 'zod'; import { fileURLToPath } from 'url'; import path from 'path'; // Import our existing handlers and services 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 { augmentWithAttributes } from '../ragno/augmentWithAttributes.js'; import { aggregateCommunities } from '../ragno/aggregateCommunities.js'; import Config from '../Config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Define schemas for our methods const SearchRequestSchema = z.object({ query: z.string(), limit: z.number().optional().default(10) }); const EmbeddingRequestSchema = z.object({ text: z.string() }); // Create MCP server with handlers export function createMCPServer() { // Initialize server with basic info const server = new Server( { name: 'semem/mcp', version: '1.0.0' }, { capabilities: { resources: { subscribe: true }, tools: {}, completions: {} } } ); // Initialize services const config = new Config(path.join(process.cwd(), 'config', 'config.json')); await config.init(); const cacheManager = new CacheManager({ maxSize: 1000, ttl: 3600000 // 1 hour }); // Initialize LLM provider based on config const llmProvider = initializeLLMProvider(config); const llmHandler = new LLMHandler(llmProvider, cacheManager); const embeddingHandler = new EmbeddingHandler(llmProvider, cacheManager); const sparqlHelpers = new SPARQLHelpers(config.get('sparqlEndpoints')); const searchService = new SearchService(sparqlHelpers, embeddingHandler); // Track subscriptions const subscriptions = new Set(); let updateInterval; // Register RPC methods with proper schemas server.setRequestHandler('semem/search', SearchRequestSchema, async (request) => { try { const { query, limit } = request.params; const results = await searchService.search(query, { limit }); return { results }; } catch (error) { console.error('Search error:', error); throw new Error(`Search failed: ${error.message}`); } }); server.setRequestHandler('semem/generateEmbedding', EmbeddingRequestSchema, async (request) => { try { const { text } = request.params; const embedding = await embeddingHandler.generateEmbedding(text); return { embedding }; } catch (error) { console.error('Embedding generation error:', error); throw new Error(`Embedding generation failed: ${error.message}`); } }); // Start the update interval updateInterval = setInterval(() => { for (const uri of subscriptions) { server.notification({ method: 'notifications/resources/updated', params: { uri } }); } }, 30000); // Cleanup function async function cleanup() { if (updateInterval) { clearInterval(updateInterval); } } return { server, cleanup }; } /** * Initialize the appropriate LLM provider based on config */ async function initializeLLMProvider(config) { const providers = (config.get('llmProviders') || []).sort((a, b) => (a.priority || 999) - (b.priority || 999)); for (const provider of providers) { try { switch (provider.type.toLowerCase()) { case 'ollama': const { OllamaConnector } = await import('../connectors/OllamaConnector.js'); return new OllamaConnector(provider); case 'claude': const { ClaudeConnector } = await import('../connectors/ClaudeConnector.js'); return new ClaudeConnector(provider); case 'mistral': const { MistralConnector } = await import('../connectors/MistralConnector.js'); return new MistralConnector(provider); } } catch (error) { console.warn(`Failed to initialize ${provider.type} provider:`, error); } } throw new Error('No valid LLM provider could be initialized'); } // Start server based on command line arguments async function main() { const args = process.argv.slice(2); const mode = args[0] || 'http'; try { const { server, cleanup } = await createMCPServer(); // Handle different transport modes if (mode === 'stdio') { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP server running in STDIO mode'); } else if (mode === 'sse') { const app = express(); const httpServer = http.createServer(app); const port = process.env.MCP_PORT || 4100; app.use(express.json()); const transport = new SSEServerTransport({ server: httpServer, path: '/mcp' }); await server.connect(transport); httpServer.listen(port, () => { console.error(`MCP server running in SSE mode on port ${port}`); }); } else { // HTTP mode (default) const app = express(); const port = process.env.MCP_PORT || 4100; app.use(express.json()); app.post('/', async (req, res) => { try { const result = await server.handleRequest(req.body); res.json(result); } catch (error) { console.error('Request error:', error); res.status(500).json({ jsonrpc: '2.0', id: req.body?.id || null, error: { code: -32603, message: 'Internal error', data: error.message } }); } }); app.listen(port, () => { console.error(`MCP server running in HTTP mode on port ${port}`); }); } // Handle process termination process.on('SIGINT', async () => { await cleanup(); await server.close(); process.exit(0); }); } catch (error) { console.error('Failed to start MCP server:', error); process.exit(1); } } // Start the server if this file is run directly if (process.argv[1] === fileURLToPath(import.meta.url)) { main().catch(console.error); }