/**
* 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);
});