Skip to main content
Glama
index.ts•9.09 kB
/** * Houtini LM MCP Server - Plugin Architecture v1.0 * Complete plugin-based replacement of legacy switch-case system */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { LMStudioClient } from '@lmstudio/sdk'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { pathToFileURL } from 'url'; import fs from 'fs'; import { config } from './config.js'; import { PluginLoader, PluginRegistry } from './plugins/index.js'; // ES module __dirname equivalent const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); class HoutiniLMServer { private server: Server; private lmStudioClient: LMStudioClient; private pluginLoader: PluginLoader; private pluginsInitialized: boolean = false; constructor() { this.server = new Server( { name: 'houtini-lm', version: '1.0.7', description: 'Local AI development companion - unlimited analysis and generation without API costs. Preserves Claude context by offloading routine tasks to local LM Studio.', }, { capabilities: { tools: { description: 'Context preservation through local processing - use for routine tasks, save Claude for strategy' }, resources: { description: 'Local LM Studio integration with unlimited processing' }, prompts: { description: 'Three-stage prompting system with expert personas' } }, } ); // Note: Security validation will be done during server start this.lmStudioClient = new LMStudioClient({ baseUrl: config.lmStudioUrl, }); this.pluginLoader = PluginRegistry.getInstance(); this.setupHandlers(); // Error handling this.server.onerror = (error) => { // Silent error handling for MCP protocol compliance }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } /** * Validate security configuration at startup * Fails fast if environment variables are not properly configured */ private async validateSecurityConfiguration(): Promise<void> { try { const { securityConfig } = await import('./security-config.js'); const allowedDirs = securityConfig.getAllowedDirectories(); // Validate each directory exists and is accessible const fs = await import('fs'); for (const dir of allowedDirs) { try { const stat = fs.statSync(dir); if (!stat.isDirectory()) { console.warn(`[WARNING] Allowed path is not a directory: ${dir}`); } } catch (error: any) { console.warn(`[WARNING] Cannot access allowed directory: ${dir} (${error.message})`); } } } catch (error: any) { console.error('SECURITY CONFIGURATION ERROR:', error.message); console.error('Please set the LLM_MCP_ALLOWED_DIRS environment variable in your Claude Desktop configuration.'); console.error('Example: "LLM_MCP_ALLOWED_DIRS": "C:\\\\MCP,C:\\\\dev,C:\\\\Users\\\\YourName\\\\Documents"'); process.exit(1); } } /** * Initialize plugins from directories */ private async initializePlugins(): Promise<void> { if (this.pluginsInitialized) return; try { console.error('DEBUG: Starting plugin initialization...'); // Load plugins from prompts directory const promptsDir = path.join(__dirname, 'prompts'); console.error('DEBUG: Loading from:', promptsDir); await this.pluginLoader.loadPlugins(promptsDir); // Load system plugins await this.loadSystemPlugins(); this.pluginsInitialized = true; console.error('DEBUG: Plugins initialized successfully'); } catch (error) { // Silent error handling to avoid JSON-RPC interference throw error; } } /** * Load system plugins from the system directory */ private async loadSystemPlugins(): Promise<void> { try { const systemDir = path.join(__dirname, 'system'); const files = fs.readdirSync(systemDir); for (const file of files) { if (file.endsWith('.js')) { // Only load .js files, skip .d.ts const filePath = path.join(systemDir, file); await this.loadSystemPlugin(filePath); } } } catch (error) { // Silent error handling to avoid JSON-RPC interference // console.error('[Plugin Server] Error loading system plugins:', error); } } /** * Load a single system plugin */ private async loadSystemPlugin(filePath: string): Promise<void> { try { // Use ES module dynamic import with proper URL const fileUrl = pathToFileURL(filePath).href; const module = await import(fileUrl); const PluginClass = module.default || module.HealthCheckPlugin || module.PathResolverPlugin || Object.values(module)[0]; if (PluginClass && typeof PluginClass === 'function') { const plugin = new PluginClass(); this.pluginLoader.registerPlugin(plugin); // Removed console.log to avoid JSON-RPC interference } } catch (error) { // Silent error handling to avoid JSON-RPC interference // console.error(`[Plugin Server] Error loading system plugin ${filePath}:`, error); } } /** * Setup MCP request handlers */ private setupHandlers(): void { // Tool listing handler - returns plugin-generated tool definitions this.server.setRequestHandler(ListToolsRequestSchema, async () => { if (!this.pluginsInitialized) { await this.initializePlugins(); } const tools = this.pluginLoader.getPlugins().map(plugin => plugin.getToolDefinition()); // Silent operation - no console output return { tools }; }); // Resources handler (empty for now) this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [], })); // Prompts handler (empty for now) this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [], })); // Main tool handler - routes all calls through plugin system this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name: toolName, arguments: args } = request.params; // Silent operation - no console output unless error if (!this.pluginsInitialized) { await this.initializePlugins(); } try { // Strip the houtini-lm: prefix to get the actual plugin name const pluginName = toolName.replace(/^houtini-lm:/, ''); // Execute plugin const result = await this.pluginLoader.executePlugin(pluginName, args, this.lmStudioClient); // Silent success - no console output // Return standardized MCP response return { content: [ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) } ] }; } catch (error: any) { // Silent error handling - only return error response without logging // Return error as MCP response return { content: [ { type: 'text', text: JSON.stringify({ error: true, message: error.message || 'Tool execution failed', tool: toolName, timestamp: new Date().toISOString() }, null, 2) } ] }; } }); } /** * Get plugin statistics */ private getPluginStats(): any { const plugins = this.pluginLoader.getPlugins(); const categories = { analyze: this.pluginLoader.getPluginsByCategory('analyze').length, generate: this.pluginLoader.getPluginsByCategory('generate').length, system: this.pluginLoader.getPluginsByCategory('system').length }; return { totalPlugins: plugins.length, categories, pluginNames: plugins.map(p => p.name) }; } /** * Start the server */ async start(): Promise<void> { // SECURITY: Validate configuration before starting server await this.validateSecurityConfiguration(); // Silent startup after security validation - no console output to avoid JSON-RPC interference const transport = new StdioServerTransport(); await this.server.connect(transport); // Server started silently } } // Start the server const server = new HoutiniLMServer(); server.start().catch((error) => { // Silent error handling - only exit on critical startup failure process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/houtini-ai/lm'

If you have feedback or need assistance with the MCP directory API, please join our Discord server