UNPKG

codecompass

Version:

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

258 lines (230 loc) 9.69 kB
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const path = require('path'); const axios = require('axios'); const git = require('isomorphic-git'); const fs = require('fs'); const { QdrantClient } = require('@qdrant/js-client-rest'); const z = require('zod'); // Configuration const repoPath = process.argv[2]; if (!repoPath) { console.error('Please provide a git repository path as an argument.'); process.exit(1); } const ollamaUrl = 'http://localhost:11434'; const qdrantUrl = 'http://localhost:6333'; const collectionName = 'code_chunks'; const chunkSize = 10; const buildTime = new Date().toISOString(); // Record build time for diff tracking // Check Ollama availability async function checkOllama() { try { await axios.get(ollamaUrl); return true; } catch (err) { console.error('Ollama is not running. Please start it with: ollama serve'); process.exit(1); } } // Generate embeddings using Ollama async function generateEmbeddings(chunks) { const embeddings = []; for (const chunk of chunks) { try { const response = await axios.post(`${ollamaUrl}/api/embeddings`, { model: 'nomic-embed-text:v1.5', prompt: chunk }); embeddings.push(response.data.embedding); } catch (err) { console.error('Error generating embedding:', err.message); throw err; } } return embeddings; } // Generate with LLM async function generateWithLLM(prompt) { try { const response = await axios.post(`${ollamaUrl}/api/generate`, { model: 'llama3.1:8b', prompt: prompt, stream: false }); return response.data.response; } catch (err) { console.error('Error generating with LLM:', err.message); throw err; } } // Generate file structure async function generateFileStructure(dir) { const files = await git.listFiles({ fs, dir, ref: 'HEAD' }); const tree = {}; for (const file of files) { const parts = file.split('/'); let current = tree; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (i === parts.length - 1) { current[part] = null; } else { current[part] = current[part] || {}; current = current[part]; } } } function formatTree(obj, indent = '') { return Object.keys(obj).map(key => { const value = obj[key]; if (value === null) return `${indent}${key}`; return `${indent}${key}/\n${formatTree(value, indent + ' ')}`; }).join('\n'); } return formatTree(tree); } // Determine if a file is documentation function isDocumentationFile(file) { const docPatterns = ['README.md', 'docs/']; return docPatterns.some(pattern => file.startsWith(pattern) || file.toLowerCase() === pattern.toLowerCase()); } // Build vector database async function buildVectorDatabase() { const client = new QdrantClient({ url: qdrantUrl }); try { await client.getCollections(); } catch (err) { console.error('Qdrant is not running. Please start it with: docker run -p 6333:6333 qdrant/qdrant'); process.exit(1); } await client.deleteCollection(collectionName); await client.createCollection(collectionName, { vectors: { size: 768, distance: 'Cosine' } }); const files = await git.listFiles({ fs, dir: repoPath, ref: 'HEAD' }); const points = []; for (const file of files) { const { blob } = await git.readBlob({ fs, dir: repoPath, oid: 'HEAD', filepath: file }); const content = Buffer.from(blob).toString('utf8'); const lines = content.split('\n'); for (let i = 0; i < lines.length; i += chunkSize) { const chunkLines = lines.slice(i, i + chunkSize); const chunk = chunkLines.join('\n'); if (chunk.trim()) { const type = isDocumentationFile(file) ? 'doc' : 'code'; points.push({ file, start_line: i + 1, end_line: i + chunkLines.length, content: chunk, type }); } } } const batchSize = 50; for (let i = 0; i < points.length; i += batchSize) { const batch = points.slice(i, i + batchSize); const embeddings = await generateEmbeddings(batch.map(p => p.content)); const qdrantPoints = batch.map((point, idx) => ({ id: `${point.file}:${point.start_line}-${point.end_line}`, vector: embeddings[idx], payload: { file: point.file, start_line: point.start_line, end_line: point.end_line, type: point.type } })); await client.upsert(collectionName, { points: qdrantPoints }); } } // Read chunk from file async function readChunk(file, startLine, endLine) { const { blob } = await git.readBlob({ fs, dir: repoPath, oid: 'HEAD', filepath: file }); const content = Buffer.from(blob).toString('utf8'); const lines = content.split('\n'); return lines.slice(startLine - 1, endLine).join('\n'); } // Search code function async function searchCode(query) { const queryEmbedding = await generateEmbeddings([query]); const client = new QdrantClient({ url: qdrantUrl }); const results = await client.search(collectionName, { vector: queryEmbedding[0], limit: 5 }); return results; } // Main function async function main() { console.log('Checking Ollama availability...'); await checkOllama(); console.log('Building vector database...'); await buildVectorDatabase(); console.log('Setting up MCP server...'); const server = new McpServer({ name: 'git-repo-analyzer', version: '1.0.0' }); // Resource: repo://structure server.resource('repo://structure', async () => ({ content: [{ type: 'text', text: await generateFileStructure(repoPath) }] })); // Resource: repo://files/* server.resource( uri => uri.startsWith('repo://files/'), async (request) => { const filePath = request.uri.replace('repo://files/', ''); const { blob } = await git.readBlob({ fs, dir: repoPath, oid: 'HEAD', filepath: filePath }); return { content: [{ type: 'text', text: Buffer.from(blob).toString('utf8') }] }; } ); // Tool: search_code server.tool('search_code', { query: z.string() }, async ({ query }) => { const results = await searchCode(query); const formatted = await Promise.all(results.map(async result => { const { file, start_line, end_line } = result.payload; const code = await readChunk(file, start_line, end_line); return `File: ${file}\nLines: ${start_line}-${end_line}\nCode:\n${code}`; })); return { content: [{ type: 'text', text: formatted.join('\n\n') || 'No results found.' }] }; }); // Tool: generate_suggestion server.tool('generate_suggestion', { query: z.string() }, async ({ query }) => { const results = await searchCode(query); const snippets = await Promise.all(results.map(async result => { const { file, start_line, end_line } = result.payload; return await readChunk(file, start_line, end_line); })); const prompt = `Based on the following code snippets from the repository, provide a suggestion or completion for the query: "${query}"\n\nCode Snippets:\n${snippets.join('\n\n')}\n\nSuggestion:`; const suggestion = await generateWithLLM(prompt); return { content: [{ type: 'text', text: suggestion }] }; }); // Tool: get_repository_context server.tool('get_repository_context', { query: z.string() }, async ({ query }) => { const results = await searchCode(query); const docSnippets = []; const codeSnippets = []; for (const result of results) { const { file, start_line, end_line, type } = result.payload; const content = await readChunk(file, start_line, end_line); if (type === 'doc') { docSnippets.push({ file, content }); } else { codeSnippets.push({ file, start_line, end_line, content }); } } // Summarize code snippets const codeSummaries = await Promise.all(codeSnippets.map(async snippet => { const summaryPrompt = `Summarize the following code snippet in one or two sentences, focusing on its main functionality or purpose.\n\nCode:\n${snippet.content}\n\nSummary:`; const summary = await generateWithLLM(summaryPrompt).catch(() => 'Unable to summarize this snippet.'); return `File: ${snippet.file}, Lines: ${snippet.start_line}-${snippet.end_line}\nSummary: ${summary}`; })); // Format documentation snippets const docFormatted = docSnippets.map(snippet => `File: ${snippet.file}\nContent:\n${snippet.content}`).join('\n\n'); // Construct the prompt let prompt; if (docSnippets.length === 0 && codeSnippets.length === 0) { prompt = `You are assisting with a coding task in a repository. The user has asked: "${query}" Unfortunately, no relevant documentation or code snippets were found in the repository for this query. Please try to answer based on general knowledge or ask for more information.`; } else { const docSection = docSnippets.length > 0 ? `Documentation:\n${docFormatted}\n\n` : ''; const codeSection = codeSummaries.length > 0 ? `Code Summaries:\n${codeSummaries.join('\n\n')}\n\n` : ''; prompt = `You are assisting with a coding task in a repository. The user has asked: "${query}" The repository context was last updated on ${buildTime}. Here is some relevant context from the repository: ${docSection}${codeSection}Based on this context, please provide guidance or code suggestions.`; } return { content: [{ type: 'text', text: prompt }] }; }); const transport = new StdioServerTransport(); await server.connect(transport); console.log('MCP server running. Connect via stdio.'); } main().catch(err => { console.error('Error:', err); process.exit(1); });