@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
806 lines (802 loc) • 72.5 kB
JavaScript
"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",