UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

806 lines (802 loc) 72.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.normalizeToolParams = normalizeToolParams; exports.startServer = startServer; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); // Added ResourceTemplate // Assuming these are correctly exported by the SDK, either from root or via defined subpaths. // If the SDK's "exports" map points these subpaths to .js files, add .js here. // If they are re-exported from the main SDK entry, use that. const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); // SessionManager import removed as it's not used or found at the specified path. // Session handling is managed by StreamableHTTPServerTransport options. const crypto_1 = require("crypto"); const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); const axios_1 = __importDefault(require("axios")); // Add this import const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const isomorphic_git_1 = __importDefault(require("isomorphic-git")); const config_service_1 = require("./config-service"); const zod_1 = require("zod"); const ollama_1 = require("./ollama"); const qdrant_1 = require("./qdrant"); const query_refinement_1 = require("./query-refinement"); // Keep this const repository_1 = require("./repository"); const llm_provider_1 = require("./llm-provider"); const agent_service_1 = require("./agent-service"); const version_1 = require("./version"); const state_1 = require("./state"); // Helper type for server startup errors class ServerStartupError extends Error { constructor(message, exitCode = 1) { super(message); this.exitCode = exitCode; this.name = "ServerStartupError"; } } function normalizeToolParams(params) { if (typeof params === 'object' && params !== null) { // Ensure it's a standard object, not a null-prototype one. // Spread syntax creates a new object with a standard prototype. return { ...params }; } if (typeof params === 'string') { try { const parsed = JSON.parse(params); if (typeof parsed === 'object' && parsed !== null) { return parsed; } return { query: params }; } catch { return { query: params }; } } if (params === null || params === undefined) { return { query: "" }; } // At this point, params can be boolean, number, bigint, or symbol. // For these types, String() or .toString() is the correct and safe way to convert. if (typeof params === 'number' || typeof params === 'boolean' || typeof params === 'bigint') { return { query: String(params) }; } if (typeof params === 'symbol') { return { query: params.toString() }; // Symbols require .toString() } // Fallback for any other unexpected type, though TS should prevent this with `unknown` config_service_1.logger.warn(`normalizeToolParams: Encountered unexpected param type at end of function: ${typeof params}. Defaulting query string.`); return { query: `[Unexpected type: ${typeof params}]` }; } // Add this function definition at the module level, before startServer // eslint-disable-next-line @typescript-eslint/require-await async function configureMcpServerInstance(mcpInstance, qdrantClient, repoPath, suggestionModelAvailable // Add other dependencies like VERSION if needed by resource/tool registration ) { // Register resources if (typeof mcpInstance.resource !== "function") { throw new Error("MCP server instance does not support 'resource' method"); } mcpInstance.resource("Server Health Status", "repo://health", async () => { const healthUri = "repo://health"; try { let ollamaStatus = "unhealthy"; try { await (0, ollama_1.checkOllama)(); ollamaStatus = "healthy"; } catch (err) { config_service_1.logger.warn(`Ollama health check failed during repo://health: ${err instanceof Error ? err.message : String(err)}`); } let qdrantStatus = "unhealthy"; try { await qdrantClient.getCollections(); qdrantStatus = "healthy"; } catch (err) { config_service_1.logger.warn(`Qdrant health check failed during repo://health: ${err instanceof Error ? err.message : String(err)}`); } const repositoryStatus = await (0, repository_1.validateGitRepository)(repoPath) ? "healthy" : "unhealthy"; const status = { ollama: ollamaStatus, qdrant: qdrantStatus, repository: repositoryStatus, version: version_1.VERSION, // Ensure VERSION is imported and accessible timestamp: new Date().toISOString() }; return { contents: [{ uri: healthUri, text: JSON.stringify(status, null, 2) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); config_service_1.logger.error(`Critical error in repo://health resource handler: ${errorMessage}`); const errorPayload = { error: "Failed to retrieve complete health status due to a critical error.", details: errorMessage, version: version_1.VERSION, // Ensure VERSION is imported and accessible timestamp: new Date().toISOString(), ollama: "unknown", qdrant: "unknown", repository: "unknown" }; return { contents: [{ uri: healthUri, text: JSON.stringify(errorPayload, null, 2) }] }; } }); mcpInstance.resource("Server Version", "repo://version", () => { return { contents: [{ uri: "repo://version", text: version_1.VERSION }] }; // Ensure VERSION is imported and accessible }); mcpInstance.resource("Repository File Structure", "repo://structure", async () => { const uriStr = "repo://structure"; const isGitRepo = await (0, repository_1.validateGitRepository)(repoPath); if (!isGitRepo) { return { contents: [{ uri: uriStr, text: "" }] }; } try { const files = await isomorphic_git_1.default.listFiles({ fs: promises_1.default, dir: repoPath, gitdir: path_1.default.join(repoPath, ".git"), ref: "HEAD" }); return { contents: [{ uri: uriStr, text: files.join("\n") }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); config_service_1.logger.error(`Error listing repository files for ${repoPath}: ${errorMessage}`); return { contents: [{ uri: uriStr, text: "", error: `Failed to list repository files: ${errorMessage}` }] }; } }); mcpInstance.resource("Repository File Content", new mcp_js_1.ResourceTemplate("repo://files/{filepath}", { list: undefined }), {}, async (uri, variables, _extra) => { const rawFilepathValue = variables.filepath; let relativeFilepath = ''; if (typeof rawFilepathValue === 'string') { relativeFilepath = rawFilepathValue.trim(); } else if (Array.isArray(rawFilepathValue) && rawFilepathValue.length > 0 && typeof rawFilepathValue[0] === 'string') { config_service_1.logger.warn(`Filepath parameter '${JSON.stringify(rawFilepathValue)}' resolved to an array. Using the first element: '${rawFilepathValue[0]}'`); relativeFilepath = rawFilepathValue[0].trim(); } else if (rawFilepathValue !== undefined) { config_service_1.logger.warn(`Filepath parameter '${Array.isArray(rawFilepathValue) ? JSON.stringify(rawFilepathValue) : rawFilepathValue}' resolved to an unexpected type: ${typeof rawFilepathValue}. Treating as empty.`); } if (!relativeFilepath) { const errMsg = "File path cannot be empty."; config_service_1.logger.error(`Error accessing resource for URI ${uri.toString()}: ${errMsg}`); return { contents: [{ uri: uri.toString(), text: "", error: errMsg }] }; } try { const resolvedRepoPath = path_1.default.resolve(repoPath); const requestedFullPath = path_1.default.resolve(repoPath, relativeFilepath); if (!requestedFullPath.startsWith(resolvedRepoPath + path_1.default.sep) && requestedFullPath !== resolvedRepoPath) { throw new Error(`Access denied: Path '${relativeFilepath}' attempts to traverse outside the repository directory.`); } let finalPathToRead = requestedFullPath; try { const stats = await promises_1.default.lstat(requestedFullPath); if (stats.isSymbolicLink()) { const symlinkTargetPath = await promises_1.default.realpath(requestedFullPath); if (!path_1.default.resolve(symlinkTargetPath).startsWith(resolvedRepoPath + path_1.default.sep) && path_1.default.resolve(symlinkTargetPath) !== resolvedRepoPath) { throw new Error(`Access denied: Symbolic link '${relativeFilepath}' points outside the repository directory.`); } finalPathToRead = symlinkTargetPath; } else if (!stats.isFile()) { throw new Error(`Access denied: Path '${relativeFilepath}' is not a file.`); } } catch (statError) { if (statError.code === 'ENOENT') { throw new Error(`File not found: ${relativeFilepath}`); } throw statError; } const content = await promises_1.default.readFile(finalPathToRead, "utf8"); return { contents: [{ uri: uri.toString(), text: content }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); config_service_1.logger.error(`Error accessing resource for URI ${uri.toString()} (relative path: ${relativeFilepath}): ${errorMessage}`); return { contents: [{ uri: uri.toString(), text: "", error: errorMessage }] }; } }); // Assuming registerTools and registerPrompts are defined elsewhere and correctly use mcpInstance registerTools(mcpInstance, qdrantClient, repoPath, suggestionModelAvailable); registerPrompts(mcpInstance); mcpInstance.tool("bb7_get_indexing_status", "Retrieves the current status of repository indexing. Provides information on whether indexing is idle, in-progress, completed, or failed, along with progress percentage and any error messages.", {}, (_args, _extra) => { config_service_1.logger.info("Tool 'get_indexing_status' execution started."); const currentStatus = (0, repository_1.getGlobalIndexingStatus)(); return { content: [{ type: "text", text: `# Indexing Status - Status: ${currentStatus.status} - Progress: ${currentStatus.overallProgress}% - Message: ${currentStatus.message} - Last Updated: ${currentStatus.lastUpdatedAt} ${currentStatus.currentFile ? `- Current File: ${currentStatus.currentFile}` : ''} ${currentStatus.currentCommit ? `- Current Commit: ${currentStatus.currentCommit}` : ''} ${currentStatus.totalFilesToIndex ? `- Total Files: ${currentStatus.totalFilesToIndex}` : ''} ${currentStatus.filesIndexed ? `- Files Indexed: ${currentStatus.filesIndexed}` : ''} ${currentStatus.totalCommitsToIndex ? `- Total Commits: ${currentStatus.totalCommitsToIndex}` : ''} ${currentStatus.commitsIndexed ? `- Commits Indexed: ${currentStatus.commitsIndexed}` : ''} ${currentStatus.errorDetails ? `- Error: ${currentStatus.errorDetails}` : ''} `, }], }; }); mcpInstance.tool("bb7_switch_suggestion_model", "Switches the primary model and provider used for generating suggestions. Embeddings continue to be handled by the configured Ollama embedding model. \nExample: To switch to 'deepseek-coder' (DeepSeek provider), use `{\"model\": \"deepseek-coder\", \"provider\": \"deepseek\"}`. To switch to 'llama3.1:8b' (Ollama provider), use `{\"model\": \"llama3.1:8b\", \"provider\": \"ollama\"}`. If provider is omitted, it may be inferred for known model patterns. For other providers like 'openai', 'gemini', 'claude', specify both model and provider: `{\"model\": \"gpt-4\", \"provider\": \"openai\"}`.", { model: zod_1.z.string().describe("The suggestion model to switch to (e.g., 'llama3.1:8b', 'deepseek-coder', 'gpt-4')."), provider: zod_1.z.string().optional().describe("The LLM provider for the model (e.g., 'ollama', 'deepseek', 'openai', 'gemini', 'claude'). If omitted, an attempt will be made to infer it.") }, async (args, _extra) => { config_service_1.logger.info("Received args for switch_suggestion_model", { args }); const modelToSwitchTo = args.model; const providerToSwitchTo = args.provider?.toLowerCase(); if (!modelToSwitchTo || typeof modelToSwitchTo !== 'string' || modelToSwitchTo.trim() === "") { const errorMsg = "Invalid or missing 'model' parameter. Please provide a non-empty model name string."; config_service_1.logger.error(errorMsg, { receivedModel: modelToSwitchTo }); return { content: [{ type: "text", text: `# Error Switching Suggestion Model\n\n${errorMsg}`, }], }; } if (args.provider !== undefined && (typeof args.provider !== 'string' || args.provider.trim() === "")) { const errorMsg = "Invalid 'provider' parameter. If provided, it must be a non-empty string."; config_service_1.logger.error(errorMsg, { receivedProvider: args.provider }); return { content: [{ type: "text", text: `# Error Switching Suggestion Model\n\n${errorMsg}`, }], }; } config_service_1.logger.info(`Requested model switch: Model='${modelToSwitchTo}', Provider='${providerToSwitchTo || "(infer)"}'`); try { const success = await (0, llm_provider_1.switchSuggestionModel)(modelToSwitchTo, providerToSwitchTo); if (!success) { return { content: [{ type: "text", text: `# Failed to Switch Suggestion Model\n\nUnable to switch to model '${modelToSwitchTo}'${providerToSwitchTo ? ` with provider '${providerToSwitchTo}'` : ''}. Please check your configuration and server logs for details. Ensure the provider is supported and any necessary API keys or host configurations are correctly set.`, }], }; } const actualModel = config_service_1.configService.SUGGESTION_MODEL; const actualProvider = config_service_1.configService.SUGGESTION_PROVIDER; const embeddingProvider = config_service_1.configService.EMBEDDING_PROVIDER; config_service_1.logger.info(`Successfully switched. ConfigService reports: Model='${actualModel}', Provider='${actualProvider}', Embedding Provider='${embeddingProvider}'`); let message = `# Suggestion Model Switched\n\nSuccessfully switched to model '${actualModel}' using provider '${actualProvider}' for suggestions.\nEmbeddings continue to use '${embeddingProvider}'.\n\n`; message += `To make this change permanent, update your environment variables (e.g., SUGGESTION_MODEL='${actualModel}', SUGGESTION_PROVIDER='${actualProvider}') or the relevant configuration files (e.g., ~/.codecompass/model-config.json).`; if (actualProvider === 'deepseek' && !config_service_1.configService.DEEPSEEK_API_KEY) { message += `\n\nWarning: DeepSeek provider is selected, but DEEPSEEK_API_KEY is not found in current configuration. Ensure it is set for DeepSeek to function.`; } else if (actualProvider === 'openai' && !config_service_1.configService.OPENAI_API_KEY) { message += `\n\nWarning: OpenAI provider is selected, but OPENAI_API_KEY is not found. Ensure it is set.`; } else if (actualProvider === 'gemini' && !config_service_1.configService.GEMINI_API_KEY) { message += `\n\nWarning: Gemini provider is selected, but GEMINI_API_KEY is not found. Ensure it is set.`; } else if (actualProvider === 'claude' && !config_service_1.configService.CLAUDE_API_KEY) { message += `\n\nWarning: Claude provider is selected, but CLAUDE_API_KEY is not found. Ensure it is set.`; } return { content: [{ type: "text", text: message, }], }; } catch (error) { config_service_1.logger.error("Error switching suggestion model", { message: error instanceof Error ? error.message : String(error) }); return { content: [{ type: "text", text: `# Error Switching Suggestion Model\n\n${error instanceof Error ? error.message : String(error)}`, }], }; } }); } async function startServer(repoPath) { process.on('uncaughtException', (error) => { config_service_1.logger.error('UNCAUGHT EXCEPTION:', { message: error.message, stack: error.stack }); if (process.env.NODE_ENV !== 'test') { process.exit(1); } }); process.on('unhandledRejection', (reason, promise) => { config_service_1.logger.error('UNHANDLED PROMISE REJECTION:', { reason, promise }); if (process.env.NODE_ENV !== 'test') { process.exit(1); } }); config_service_1.logger.info("Starting CodeCompass MCP server..."); // eslint-disable-next-line @typescript-eslint/no-empty-function -- Initializing with a no-op, will be reassigned. let httpServerSetupReject = () => { }; const httpServerSetupPromise = new Promise((_resolve, reject) => { httpServerSetupReject = reject; }); try { config_service_1.configService.reloadConfigsFromFile(true); config_service_1.logger.info(`Initial suggestion model from config: ${config_service_1.configService.SUGGESTION_MODEL}`); if (!repoPath || repoPath === "${workspaceFolder}" || repoPath.trim() === "") { config_service_1.logger.warn("Invalid repository path provided, defaulting to current directory"); repoPath = process.cwd(); } const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const isLlmAvailable = await llmProvider.checkConnection(); if (!isLlmAvailable) { config_service_1.logger.warn(`LLM provider (${config_service_1.configService.SUGGESTION_PROVIDER}) is not available. Some features may not work.`); } let suggestionModelAvailable = false; try { const currentSuggestionProvider = config_service_1.configService.SUGGESTION_PROVIDER.toLowerCase(); if (currentSuggestionProvider === 'ollama') { await (0, ollama_1.checkOllama)(); // Assumes checkOllama is imported await (0, ollama_1.checkOllamaModel)(config_service_1.configService.EMBEDDING_MODEL, true); // Assumes checkOllamaModel is imported await (0, ollama_1.checkOllamaModel)(config_service_1.configService.SUGGESTION_MODEL, false); suggestionModelAvailable = true; } else if (currentSuggestionProvider === 'deepseek') { suggestionModelAvailable = isLlmAvailable; } else { suggestionModelAvailable = isLlmAvailable; } } catch (error) { config_service_1.logger.warn(`Warning: Model not available. Suggestion tools may be limited: ${error.message}`); } const qdrantClient = await (0, qdrant_1.initializeQdrant)(); config_service_1.logger.info(`Initial indexing process started for ${repoPath} in the background.`); (0, repository_1.indexRepository)(qdrantClient, repoPath, llmProvider) .then(() => { config_service_1.logger.info(`Initial indexing process completed successfully for ${repoPath}.`); }) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); config_service_1.logger.error(`Initial indexing process failed for ${repoPath}: ${errorMessage}`); }); const serverCapabilities = { resources: { "repo://structure": { name: "Repository File Structure", description: "Lists all files in the current Git repository.", mimeType: "text/plain" }, "repo://files/{filepath}": { name: "Repository File Content", description: "Retrieves the content of a specific file...", mimeType: "text/plain", template: true, parameters: { filepath: { type: "string", description: "..." } } }, "repo://health": { name: "Server Health Status", description: "Provides the health status...", mimeType: "application/json" }, "repo://version": { name: "Server Version", description: "Provides the current version...", mimeType: "text/plain" } }, tools: { bb7_search_code: {}, bb7_get_repository_context: {}, ...(suggestionModelAvailable ? { bb7_generate_suggestion: {} } : {}), bb7_get_changelog: {}, bb7_agent_query: {}, bb7_switch_suggestion_model: {}, bb7_get_indexing_status: {}, }, prompts: { "bb7_repository-context": {}, "bb7_code-suggestion": {}, "bb7_code-analysis": {} }, }; // This McpServer instance is primarily for defining capabilities. // Per-session instances will be created for actual MCP communication. const _globalMcpServer = new mcp_js_1.McpServer({ name: "CodeCompass", version: version_1.VERSION, vendor: "CodeCompass", capabilities: serverCapabilities, }); // Resource/tool/prompt registration for the global server instance is not strictly necessary // if all MCP communication goes through per-session instances that are configured individually. // However, if any global handlers were intended, they would be registered on _globalMcpServer. // For now, configureMcpServerInstance will be called on per-session servers. const finalDeclaredTools = Object.keys(serverCapabilities.tools); config_service_1.logger.info(`Declared tools in capabilities: ${finalDeclaredTools.join(', ')}`); const finalDeclaredPrompts = Object.keys(serverCapabilities.prompts); config_service_1.logger.info(`Declared prompts in capabilities: ${finalDeclaredPrompts.join(', ')}`); const expressApp = (0, express_1.default)(); expressApp.use(express_1.default.json()); expressApp.get('/api/indexing-status', (_req, res) => { res.json((0, repository_1.getGlobalIndexingStatus)()); }); expressApp.get('/api/ping', (_req, res) => { res.json({ service: "CodeCompass", status: "ok", version: version_1.VERSION }); }); expressApp.post('/api/repository/notify-update', (_req, res) => { config_service_1.logger.info('Received /api/repository/notify-update.'); const currentStatus = (0, repository_1.getGlobalIndexingStatus)(); if (['initializing', 'validating_repo', 'listing_files', 'cleaning_stale_entries', 'indexing_file_content', 'indexing_commits_diffs'].includes(currentStatus.status)) { res.status(409).json({ message: 'Indexing already in progress.' }); return; } (0, repository_1.indexRepository)(qdrantClient, repoPath, llmProvider).catch(err => config_service_1.logger.error("Re-indexing error:", err)); res.status(202).json({ message: 'Re-indexing initiated.' }); }); const activeSessionTransports = new Map(); expressApp.post('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; let transport = sessionId ? activeSessionTransports.get(sessionId) : undefined; if (transport) { config_service_1.logger.debug(`MCP POST: Reusing transport for session ${sessionId}`); } else if ((0, types_js_1.isInitializeRequest)(req.body)) { config_service_1.logger.info('MCP POST: Initialization request, creating new transport and server instance.'); const newTransport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: crypto_1.randomUUID, onsessioninitialized: (newSessionId) => { activeSessionTransports.set(newSessionId, newTransport); config_service_1.logger.info(`MCP Session initialized: ${newSessionId}`); } }); newTransport.onclose = () => { if (newTransport.sessionId) { activeSessionTransports.delete(newTransport.sessionId); config_service_1.logger.info(`MCP Session closed and transport removed: ${newTransport.sessionId}`); } }; const sessionServer = new mcp_js_1.McpServer({ name: "CodeCompass", version: version_1.VERSION, vendor: "CodeCompass", capabilities: serverCapabilities, }); await configureMcpServerInstance(sessionServer, qdrantClient, repoPath, suggestionModelAvailable); await sessionServer.connect(newTransport); transport = newTransport; } else { config_service_1.logger.warn(`MCP POST: Bad Request. No valid session ID and not an init request.`); const bodyWithId = req.body; res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID or not an init request.' }, id: (typeof bodyWithId === 'object' && bodyWithId !== null && 'id' in bodyWithId) ? bodyWithId.id : null, }); return; } try { await transport.handleRequest(req, res, req.body); } catch (transportError) { config_service_1.logger.error("Error handling MCP POST request via transport:", transportError); if (!res.headersSent) { const bodyWithId = req.body; res.status(500).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Internal MCP transport error.' }, id: (typeof bodyWithId === 'object' && bodyWithId !== null && 'id' in bodyWithId) ? bodyWithId.id : null, }); } } }); const handleSessionRequest = async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !activeSessionTransports.has(sessionId)) { config_service_1.logger.warn(`MCP ${req.method}: Invalid or missing session ID: ${sessionId}.`); res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Invalid or missing session ID.' }, id: null }); return; } const transport = activeSessionTransports.get(sessionId); config_service_1.logger.debug(`MCP ${req.method}: Handling request for session ${sessionId}`); try { await transport.handleRequest(req, res); } catch (transportError) { config_service_1.logger.error(`Error handling MCP ${req.method} request for session ${sessionId}:`, transportError); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Internal MCP transport error.' }, id: null }); } } }; expressApp.get('/mcp', handleSessionRequest); expressApp.delete('/mcp', handleSessionRequest); config_service_1.logger.info(`MCP communication will be available at the /mcp endpoint via POST, GET, DELETE.`); const httpPort = config_service_1.configService.HTTP_PORT; const httpServer = http_1.default.createServer(expressApp); // eslint-disable-next-line @typescript-eslint/no-misused-promises httpServer.on('error', async (error) => { if (error.code === 'EADDRINUSE') { config_service_1.logger.warn(`HTTP Port ${httpPort} is already in use. Attempting to ping...`); try { const pingResponse = await axios_1.default.get(`http://localhost:${httpPort}/api/ping`, { timeout: 500 }); if (pingResponse.status === 200 && pingResponse.data?.service === "CodeCompass") { config_service_1.logger.info(`Another CodeCompass instance (v${pingResponse.data.version || 'unknown'}) is running on port ${httpPort}.`); // ... (rest of EADDRINUSE handling logic as in your file) ... // Full EADDRINUSE logic from user's provided file: try { const statusResponse = await axios_1.default.get(`http://localhost:${httpPort}/api/indexing-status`, { timeout: 1000 }); if (statusResponse.status === 200 && statusResponse.data) { const existingStatus = statusResponse.data; console.info(`\n--- Status of existing CodeCompass instance on port ${httpPort} ---`); console.info(`Version: ${pingResponse.data.version || 'unknown'}`); console.info(`Status: ${existingStatus.status}`); console.info(`Message: ${existingStatus.message}`); if (existingStatus.overallProgress !== undefined) { console.info(`Progress: ${existingStatus.overallProgress}%`); } if (existingStatus.currentFile) { console.info(`Current File: ${existingStatus.currentFile}`); } if (existingStatus.currentCommit) { console.info(`Current Commit: ${existingStatus.currentCommit}`); } console.info(`Last Updated: ${existingStatus.lastUpdatedAt}`); console.info(`-----------------------------------------------------------\n`); config_service_1.logger.info("Current instance will exit as another CodeCompass server is already running."); httpServerSetupReject(new ServerStartupError(`Port ${httpPort} in use by another CodeCompass instance.`, 0)); } else { config_service_1.logger.error(`Failed to retrieve status from existing CodeCompass server on port ${httpPort}. It responded to ping but status endpoint failed. Status: ${statusResponse.status}`); httpServerSetupReject(new ServerStartupError(`Port ${httpPort} in use, status fetch failed.`, 1)); } } catch (statusError) { if (axios_1.default.isAxiosError(statusError)) { if (statusError.response) { config_service_1.logger.error(`Error fetching status from existing CodeCompass server (port ${httpPort}): ${statusError.message}, Status: ${statusError.response.status}, Data: ${JSON.stringify(statusError.response.data)}`); } else if (statusError.request) { config_service_1.logger.error(`Error fetching status from existing CodeCompass server (port ${httpPort}): No response received. ${statusError.message}`); } else { config_service_1.logger.error(`Error fetching status from existing CodeCompass server (port ${httpPort}): ${statusError.message}`); } } else { config_service_1.logger.error(`Error fetching status from existing CodeCompass server (port ${httpPort}): ${String(statusError)}`); } httpServerSetupReject(new ServerStartupError(`Port ${httpPort} in use, status fetch error.`, 1)); } } else { config_service_1.logger.error(`Port ${httpPort} is in use by non-CodeCompass server. Response: ${JSON.stringify(pingResponse.data)}`); config_service_1.logger.error(`Please free the port or configure a different one (e.g., via HTTP_PORT environment variable or in ~/.codecompass/model-config.json).`); httpServerSetupReject(new ServerStartupError(`Port ${httpPort} in use by non-CodeCompass server.`, 1)); } } catch (pingError) { config_service_1.logger.error(`Port ${httpPort} is in use by an unknown service or the existing CodeCompass server is unresponsive to pings.`); if (axios_1.default.isAxiosError(pingError)) { if (pingError.code === 'ECONNREFUSED') { config_service_1.logger.error(`Connection refused on port ${httpPort}.`); } else if (pingError.code === 'ETIMEDOUT' || pingError.code === 'ECONNABORTED') { config_service_1.logger.error(`Ping attempt to port ${httpPort} timed out.`); } else { config_service_1.logger.error(`Ping error details: ${pingError.message}`); } } else { config_service_1.logger.error(`Ping error details: ${String(pingError)}`); } config_service_1.logger.error(`Please free the port or configure a different one (e.g., via HTTP_PORT environment variable or in ~/.codecompass/model-config.json).`); httpServerSetupReject(new ServerStartupError(`Port ${httpPort} in use or ping failed.`, 1)); } } else { config_service_1.logger.error(`Failed to start HTTP server on port ${httpPort}: ${error.message}`); httpServerSetupReject(new ServerStartupError(`HTTP server error: ${error.message}`, 1)); } }); const listenPromise = new Promise((resolve) => { httpServer.listen(httpPort, () => { config_service_1.logger.info(`CodeCompass HTTP server listening on port ${httpPort} for status and notifications.`); resolve(); }); }); await Promise.race([listenPromise, httpServerSetupPromise]); config_service_1.logger.info(`CodeCompass MCP server v${version_1.VERSION} running for repository: ${repoPath}`); console.error(`CodeCompass v${version_1.VERSION} HTTP Server running on port ${httpPort}, with MCP at /mcp`); // Changed to console.error as per user's new code if (process.env.NODE_ENV === 'test') { config_service_1.logger.info("Test environment detected, server setup complete. Skipping SIGINT wait."); } else { await new Promise((resolve) => { process.on('SIGINT', () => { config_service_1.logger.info("SIGINT received, shutting down server."); resolve(); }); }); } } catch (error) { const err = error instanceof ServerStartupError ? error : new Error(String(error)); // Ensure err is Error type config_service_1.logger.error("Failed to start CodeCompass", { message: err.message }); if (process.env.NODE_ENV === 'test') { throw err; } const exitCode = error instanceof ServerStartupError ? error.exitCode : 1; process.exit(exitCode); } } function registerPrompts(server) { if (typeof server.prompt !== "function") { config_service_1.logger.warn("MCP server instance does not support 'prompt' method. Prompts may not be available."); return; } server.prompt("bb7_repository-context", "Get context about your repository", { query: zod_1.z.string().describe("The specific topic or question for which context is needed.") }, ({ query }) => ({ messages: [{ role: "user", content: { type: "text", text: `Provide context about ${query} in this repository` } }] })); server.prompt("bb7_code-suggestion", "Generate code suggestions", { query: zod_1.z.string().describe("The specific topic or problem for which a code suggestion is needed.") }, ({ query }) => ({ messages: [{ role: "user", content: { type: "text", text: `Generate a code suggestion for: ${query}` } }] })); server.prompt("bb7_code-analysis", "Analyze code problems", { query: zod_1.z.string().describe("The code problem or snippet to be analyzed.") }, ({ query }) => ({ messages: [{ role: "user", content: { type: "text", text: `Analyze this code problem: ${query}` } }] })); } function registerTools(// Removed async server, qdrantClient, repoPath, suggestionModelAvailable) { if (typeof server.tool !== "function") { throw new Error("MCP server does not support 'tool' method"); } // Add the agent_query tool server.tool("bb7_agent_query", "Provides a detailed plan and a comprehensive summary for addressing complex questions or tasks related to the codebase. This tool generates these insights in a single pass. \nExample: `{\"query\": \"How is user authentication handled in this project?\"}`.", { query: zod_1.z.string().describe("The question or task for the agent to process"), sessionId: zod_1.z.string().optional().describe("Optional session ID to maintain context between requests") // maxSteps removed }, async (args, _extra) => { config_service_1.logger.info(`Tool 'agent_query' execution started with args:`, args); const query = args.query; const sessionId = args.sessionId; if (!query || typeof query !== 'string' || query.trim() === "") { const errorMsg = "Invalid or missing 'query' parameter for agent_query. Please provide a non-empty query string."; config_service_1.logger.error(errorMsg, { receivedQuery: query }); return { content: [{ type: "text", text: `# Agent Query Error\n\n${errorMsg}`, }], }; } try { // Ensure config is fresh for this operation, especially if models/providers might have changed // configService.reloadConfigsFromFile(true); // processAgentQuery will use current configService state // processAgentQuery will internally get the LLMProvider and QdrantClient const agentResponseText = await (0, agent_service_1.processAgentQuery)(query, sessionId); return { content: [{ type: "text", text: agentResponseText, }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); config_service_1.logger.error("Critical error in agent_query tool handler", { message: errorMessage, stack: error instanceof Error ? error.stack : undefined, }); return { content: [{ type: "text", text: `# Agent Query Failed\n\nAn unexpected error occurred: ${errorMessage}\nPlease check server logs for details.`, }], }; } }); // Tool to execute the next step of an agent's plan - REMOVED // Search Code Tool with iterative refinement server.tool("bb7_search_code", "Performs a semantic search for code snippets within the repository that are relevant to the given query. Results include file paths, code snippets, and relevance scores. \nExample: `{\"query\": \"function to handle user login\"}`. For a broader search: `{\"query\": \"database connection setup\"}`.", { query: zod_1.z.string().describe("The search query to find relevant code in the repository"), sessionId: zod_1.z.string().optional().describe("Optional session ID to maintain context between requests") }, async (args, _extra) => { config_service_1.logger.info(`Tool 'search_code' execution started.`); config_service_1.logger.info("Received args for search_code", { args }); const searchQuery = args.query || "code search"; // Default if query is empty string const searchSessionId = args.sessionId; if (args.query === undefined || args.query === null || args.query.trim() === "") { config_service_1.logger.warn("No query provided or query is empty for search_code, using default 'code search'"); } try { const session = (0, state_1.getOrCreateSession)(searchSessionId, repoPath); config_service_1.logger.info("Using query for search_code", { query: searchQuery, sessionId: session.id }); const isGitRepo = await (0, repository_1.validateGitRepository)(repoPath); const files = isGitRepo ? await isomorphic_git_1.default.listFiles({ fs: promises_1.default, dir: repoPath, gitdir: path_1.default.join(repoPath, ".git"), ref: "HEAD" }) : []; (0, state_1.updateContext)(session.id, repoPath, files); const { results, refinedQuery, relevanceScore } = await (0, query_refinement_1.searchWithRefinement)(qdrantClient, searchQuery, files); (0, state_1.addQuery)(session.id, searchQuery, results, relevanceScore); const fileChunkResults = results.filter((result) => result.payload?.dataType === 'file_chunk'); if (fileChunkResults.length === 0 && results.length > 0) { config_service_1.logger.info(`Search for "${searchQuery}" found ${results.length} results, but none were file_chunks. Matched data might be from commits or diffs.`); } else if (results.length === 0) { config_service_1.logger.info(`Search for "${searchQuery}" found no results.`); } const summaries = await Promise.all(fileChunkResults.map(async (result) => { // Now, result.payload is known to be FileChunkPayload const snippet = result.payload.file_content_chunk.slice(0, config_service_1.configService.MAX_SNIPPET_LENGTH); let summaryText = "Summary unavailable"; // Renamed from 'summary' to avoid conflict with outer scope if (suggestionModelAvailable) { try { const summarizePrompt = `Summarize this code snippet in 50 words or less:\n\n${snippet}`; // Ensure llmProvider is available in this scope. It's initialized in startServer. // If not directly available, it needs to be passed or retrieved via getLLMProvider(). // Assuming llmProvider is accessible here (e.g., passed to registerTools or retrieved). // For now, let's assume getLLMProvider() is the way if not passed down. const currentLlmProvider = await (0, llm_provider_1.getLLMProvider)(); // Get it if not passed down summaryText = await currentLlmProvider.generateText(summarizePrompt); } catch (error) { config_service_1.logger.warn(`Failed to generate summary for ${result.payload.filepath}: ${error.message}`); summaryText = "Summary generation failed"; } } return { filepath: result.payload.filepath, snippet, summary: summaryText, // Use the renamed variable last_modified: result.payload.last_modified, relevance: result.score, }; })); const formattedResponse = `# Search Results for: "${searchQuery}" ${refinedQuery !== searchQuery ? `\n> Query refined to: "${refinedQuery}"` : ''} ${summaries.length > 0 ? summaries.map(s => ` ## ${s.filepath} - Last Modified: ${s.last_modified} - Relevance: ${s.relevance.toFixed(2)} ### Code Snippet \`\`\` ${s.snippet} \`\`\` ### Summary ${s.summary} `).join('\n') : "\nNo relevant code snippets found in files for your query. The query might have matched commit messages or diffs, which are not detailed by this tool."} Session ID: ${session.id} (Use this ID in future requests to maintain context)`; return { content: [{ type: "text", text: formattedResponse, }], }; } catch (error) { config_service_1.logger.error("Error in search_code tool", { error: error instanceof Error ? error.message : String(error) }); return { content: [{ type: "text", text: `# Error in Search Code Tool\n\nThere was an unexpected error processing your query: ${error instanceof Error ? error.message : String(error)}\n\nPlease check the server logs for more details.`, }], }; } }); // Add get_changelog tool server.tool("bb7_get_changelog", // name "Retrieves the content of the `CHANGELOG.md` file from the root of the repository. This provides a history of changes and versions for the project. \nExample: Call this tool without parameters: `{}`. Title: Get Changelog", // description (Title incorporated here) {}, // paramsSchema (as ZodRawShape for no parameters) async (_args, _extra) => { try { const changelogPath = path_1.default.join(repoPath, 'CHANGELOG.md'); const changelog = await promises_1.default.readFile(changelogPath, 'utf8'); return { content: [{ type: "text", text: `# CodeCompass Changelog (v${version_1.VERSION})\n\n${changelog}`, }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); config_service_1.logger.error("Failed to read changelog", { message: errorMessage }); return { content: [{ type: "text", text: `# Error Reading Changelog\n\nFailed to read the changelog file. Current version is ${version_1.VERSION}.`, }], }; } } // The 5th argument (annotations object) has been removed. ); // Add reset_metrics tool - REMOVED // Add check_provider tool - REMOVED // Add get_session_history tool server.tool("bb7_get_session_history", "Retrieves the history of interactions (queries, suggestions, feedback) for a given session ID. This allows you to review past activities within a specific CodeCompass session. \nExample: `{\"sessionId\": \"your_session_id_here\"}`.", { sessionId: zod_1.z.string().describe("The session ID to retrieve history for") }, // Ensure this handler is synchronous if no await is used. (args, _extra) => { config_service_1.logger.info("Received args for get_session_history", { args }); const sessionIdValue = args.sessionId; if (typeof sessionIdValue !== 'string' || !sessionIdValue) { const errorMsg = "Session ID is required and must be a non-empty string."; config_service_1.logger.error(errorMsg, { receivedSessionId: String(sessionIdValue) }); // Return an error structure consistent with other tools return { content: [{ type: "text",