Skip to main content
Glama
index.ts13.2 kB
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ChildProcessWithoutNullStreams } from 'child_process'; import * as path from 'path'; import { parseCliArgs, resolveStartConfig, resolveRunConfig, handleConfigInit, handleConfigShow, handleConfigPath, StartCommandArgs, RunCommandArgs, ConfigCommandArgs, } from '../utils/cli.js'; import { createLspClient, initializeLspClient } from '../lsp-client.js'; import { openFileWithStrategy } from '../lsp/file-lifecycle/index.js'; import { createDiagnosticsStore, createDiagnosticProviderStore, createWindowLogStore, createWorkspaceLoaderStore, } from '../state/index.js'; import { DiagnosticsStore, DiagnosticProviderStore, LspClient, LspConfig, LspContext, PreloadedFiles, WindowLogStore, WorkspaceState, WorkspaceLoaderStore, } from '../types.js'; import { getLspConfig, autoDetectLsp, listAvailableLsps, ParsedLspConfig, createConfigFromDirectCommand, } from '../config/lsp-config.js'; import { getDefaultPreloadFiles } from '../utils/log-level.js'; import logger, { upgradeToContextualLogger } from '../utils/logger.js'; import { createServer } from './create-server.js'; import { setupShutdown } from './shutdown.js'; // Module-level state let lspClient: LspClient | null = null; let lspProcess: ChildProcessWithoutNullStreams | null = null; let lspName: string = ''; let lspConfiguration: ParsedLspConfig | null = null; let workspaceUri: string = ''; let workspacePath: string = ''; const preloadedFiles: PreloadedFiles = new Map(); const diagnosticsStore: DiagnosticsStore = createDiagnosticsStore(); const diagnosticProviderStore: DiagnosticProviderStore = createDiagnosticProviderStore(); const windowLogStore: WindowLogStore = createWindowLogStore(); const workspaceLoaderStore: WorkspaceLoaderStore = createWorkspaceLoaderStore(); const workspaceState: WorkspaceState = { isLoading: false, isReady: false, }; /** * Initialize LSP connection for 'start' command */ async function initializeLspForStart( cliArgs: StartCommandArgs ): Promise<void> { try { const config = resolveStartConfig(cliArgs); logger.info('Resolved configuration', { config }); // Set up workspace paths workspacePath = config.workspace; workspaceUri = `file://${path.resolve(workspacePath)}`; const workspaceName = path.basename(workspacePath); // Set log level from config (this will affect winston logger) if (process.env.SYMBOLS_LOGLEVEL !== config.loglevel) { process.env.SYMBOLS_LOGLEVEL = config.loglevel; logger.level = config.loglevel; } // Standard mode: use config file and auto-detection // Get LSP server name - try CLI args/environment, then auto-detection lspName = config.lsp || ''; if (!lspName) { // Try auto-detection based on workspace files const detectedLsp = autoDetectLsp(workspacePath, config.configPath); if (detectedLsp) { lspName = detectedLsp; logger.info('Auto-detected LSP server', { lspName, workspacePath }); } else { logger.error( 'No LSP server could be auto-detected for this workspace', { workspacePath, availableLsps: listAvailableLsps(config.configPath), } ); throw new Error( 'No LSP server specified and none could be auto-detected. ' + 'Please specify --lsp=<server> or ensure your workspace has recognizable project files.' ); } } else { const source = cliArgs.lsp ? 'CLI argument' : 'environment variable'; logger.info(`Using LSP from ${source}`, { lspName }); } // Switch to contextual logger now that we know workspace and LSP logger.debug('Upgrading to contextual logging with workspace + LSP context'); upgradeToContextualLogger(workspacePath, lspName); logger.info('Initializing LSP', { lspName, workspacePath, }); // Load LSP configuration lspConfiguration = getLspConfig( lspName, config.configPath, config.workspace ); if (!lspConfiguration) { logger.error(`LSP configuration not found for: ${lspName}`); throw new Error(`LSP configuration not found for: ${lspName}`); } logger.debug('LSP configuration loaded', { lspName, command: `${lspConfiguration.commandName} ${lspConfiguration.commandArgs.join(' ')}`, extensions: Object.keys(lspConfiguration.extensions), diagnosticsStrategy: lspConfiguration.diagnostics.strategy, diagnosticsWaitTimeout: lspConfiguration.diagnostics.wait_timeout_ms, workspaceLoader: lspConfiguration.workspace_loader || 'default', preloadFilesCount: lspConfiguration.preload_files?.length || 0, preloadFiles: lspConfiguration.preload_files && lspConfiguration.preload_files.length > 0 ? lspConfiguration.preload_files : 'none', }); const workspaceConfig: LspConfig = { workspaceUri, workspaceName, preloadFiles: lspConfiguration.preload_files || [], }; const clientResult = createLspClient( workspaceConfig, lspConfiguration, diagnosticsStore, diagnosticProviderStore, windowLogStore, workspaceLoaderStore ); if (!clientResult.ok) { throw new Error(clientResult.error.message); } const initResult = await initializeLspClient( clientResult.data.client, workspaceConfig, diagnosticProviderStore, workspaceLoaderStore, lspConfiguration ); if (!initResult.ok) { throw new Error(initResult.error.message); } lspClient = clientResult.data.client; lspProcess = clientResult.data.process; // Initialize workspace by opening preloaded files await initializeWorkspace( workspaceConfig, config.configPath, config.workspace ); } catch (error) { // Re-throw with original message - already clear and descriptive throw error instanceof Error ? error : new Error(String(error)); } } /** * Initialize LSP connection for 'run' command (direct command mode) */ async function initializeLspForRun(cliArgs: RunCommandArgs): Promise<void> { try { const config = resolveRunConfig(cliArgs); logger.info('Resolved configuration', { config }); // Set up workspace paths workspacePath = config.workspace; workspaceUri = `file://${path.resolve(workspacePath)}`; const workspaceName = path.basename(workspacePath); // Set log level from config if (process.env.SYMBOLS_LOGLEVEL !== config.loglevel) { process.env.SYMBOLS_LOGLEVEL = config.loglevel; logger.level = config.loglevel; } logger.debug('Direct command mode detected', { command: cliArgs.directCommand.commandName, args: cliArgs.directCommand.commandArgs, }); // Create minimal config from direct command lspConfiguration = createConfigFromDirectCommand( cliArgs.directCommand.commandName, cliArgs.directCommand.commandArgs ); lspName = lspConfiguration.name; logger.debug('Created LSP configuration from direct command', { lspName, command: cliArgs.directCommand.commandName, extensionsCount: Object.keys(lspConfiguration.extensions).length, }); // Switch to contextual logger upgradeToContextualLogger(workspacePath, lspName); logger.debug('LSP configuration loaded', { lspName, command: `${lspConfiguration.commandName} ${lspConfiguration.commandArgs.join(' ')}`, extensions: Object.keys(lspConfiguration.extensions), diagnosticsStrategy: lspConfiguration.diagnostics.strategy, diagnosticsWaitTimeout: lspConfiguration.diagnostics.wait_timeout_ms, workspaceLoader: lspConfiguration.workspace_loader || 'default', preloadFilesCount: lspConfiguration.preload_files?.length || 0, preloadFiles: lspConfiguration.preload_files && lspConfiguration.preload_files.length > 0 ? lspConfiguration.preload_files : 'none', }); const workspaceConfig: LspConfig = { workspaceUri, workspaceName, preloadFiles: lspConfiguration.preload_files || [], }; const clientResult = createLspClient( workspaceConfig, lspConfiguration, diagnosticsStore, diagnosticProviderStore, windowLogStore, workspaceLoaderStore ); if (!clientResult.ok) { throw new Error(clientResult.error.message); } const initResult = await initializeLspClient( clientResult.data.client, workspaceConfig, diagnosticProviderStore, workspaceLoaderStore, lspConfiguration ); if (!initResult.ok) { throw new Error(initResult.error.message); } lspClient = clientResult.data.client; lspProcess = clientResult.data.process; // Initialize workspace by opening preloaded files await initializeWorkspace( workspaceConfig, undefined, config.workspace ); } catch (error) { // Re-throw with original message - already clear and descriptive throw error instanceof Error ? error : new Error(String(error)); } } /** * Initialize workspace by opening preloaded files to trigger project loading */ async function initializeWorkspace( config: LspConfig, configPath?: string, workspacePath?: string ): Promise<void> { if (!lspClient) { throw new Error('LSP client not initialized'); } workspaceState.isLoading = true; workspaceState.loadingStartedAt = new Date(); try { // Get preloaded files from config or use defaults const filesToOpen = config.preloadFiles || getDefaultPreloadFiles(); if (filesToOpen.length === 0) { // No preloaded files specified - workspace symbol search may not work until a file is opened workspaceState.isLoading = false; workspaceState.isReady = true; workspaceState.readyAt = new Date(); return; } // Open each preloaded file for (const filePath of filesToOpen) { // Opening preloaded file: ${filePath} const result = await openFileWithStrategy( lspClient, filePath, preloadedFiles, 'persistent', configPath, workspacePath ); if (!result.ok) { // Failed to open preloaded file ${filePath}: ${result.error} } } workspaceState.isLoading = false; workspaceState.isReady = true; workspaceState.readyAt = new Date(); // Workspace initialized with ${filesToOpen.length} preloaded files } catch (error) { workspaceState.isLoading = false; throw new Error( `Failed to initialize workspace: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Factory function to create LspContext after LSP client is initialized */ function createContext(): LspContext { if (!lspClient) { throw new Error('LSP client not initialized - cannot create context'); } return { client: lspClient, preloadedFiles, diagnosticsStore, diagnosticProviderStore, windowLogStore, workspaceState, workspaceUri, workspacePath, lspName, lspConfig: lspConfiguration, }; } /** * Main entry point - routes commands and starts appropriate handlers */ export async function main(): Promise<void> { logger.debug('MCP server starting up'); // Parse CLI arguments const cliArgs = parseCliArgs(); logger.debug('Parsed CLI arguments', { command: cliArgs.command }); // Handle config subcommands (these exit after completion) if (cliArgs.command === 'config') { const configArgs = cliArgs as ConfigCommandArgs; if (configArgs.subcommandArgs.subcommand === 'init') { handleConfigInit(configArgs.subcommandArgs); process.exit(0); } else if (configArgs.subcommandArgs.subcommand === 'show') { handleConfigShow(configArgs.subcommandArgs); process.exit(0); } else if (configArgs.subcommandArgs.subcommand === 'path') { handleConfigPath(configArgs.subcommandArgs); process.exit(0); } } // Handle start command (standard MCP server mode) if (cliArgs.command === 'start') { await initializeLspForStart(cliArgs as StartCommandArgs); } // Handle run command (direct command mode) else if (cliArgs.command === 'run') { await initializeLspForRun(cliArgs as RunCommandArgs); } // No command or unknown command - show help else { console.error('Please specify a command: start, run, or config'); console.error('Run "symbols --help" for usage information'); process.exit(1); } // Ensure both client and process are available for shutdown if (!lspClient || !lspProcess) { throw new Error('LSP client or process not initialized'); } // Create and configure MCP server const server = createServer(createContext); // Set up graceful shutdown handling setupShutdown(server, lspClient, lspProcess); // Start receiving messages on stdin and sending messages on stdout logger.debug('Starting MCP server transport'); const transport = new StdioServerTransport(); await server.connect(transport); logger.info('MCP server ready'); }

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/p1va/symbols-mcp'

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