sourcegraph-mcp-server
Version:
A Model Context Protocol (MCP) server that connects AI assistants to Sourcegraph code search with natural language capabilities
400 lines (398 loc) • 15.6 kB
JavaScript
;
/**
* Enhanced result formatter with syntax highlighting and better display options
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeQueryEnhanced = exports.formatSearchResultsEnhanced = void 0;
/**
* Enhanced formatter for search results with better display formats
* including markdown code blocks with syntax highlighting
*/
function formatSearchResultsEnhanced(results, queryInfo) {
const { query, type } = queryInfo;
const matchCount = results.matchCount;
const items = results.results;
// Early return for no results
if (!items || items.length === 0) {
return `No matches found for "${query}" with type:${type}.`;
}
// Group results by repository
const repositories = {};
// Process each result item
items.forEach((item) => {
if (item.__typename === 'FileMatch') {
const repoName = item.repository.name;
const filePath = item.file.path;
// Initialize repository if not exists
if (!repositories[repoName]) {
repositories[repoName] = { name: repoName, files: {} };
}
// Initialize file array if not exists
if (!repositories[repoName].files[filePath]) {
repositories[repoName].files[filePath] = [];
}
// Add line matches
item.lineMatches.forEach((match) => {
repositories[repoName].files[filePath].push({
path: filePath,
lineNumber: match.lineNumber,
preview: match.preview
});
});
}
else if (item.__typename === 'CommitSearchResult') {
const commit = item.commit;
const repoName = commit.repository.name;
// Initialize repository if not exists
if (!repositories[repoName]) {
repositories[repoName] = { name: repoName, files: {} };
}
// Initialize commits array if not exists
if (!repositories[repoName].commits) {
repositories[repoName].commits = [];
}
// Add commit
repositories[repoName].commits.push({
oid: commit.oid,
message: commit.message,
author: commit.author.person.name,
email: commit.author.person.email,
date: commit.author.date
});
// If it's a diff search result, process the diffs
if (type === 'diff' && item.diff) {
// Initialize diffs array if not exists
if (!repositories[repoName].diffs) {
repositories[repoName].diffs = [];
}
const fileDiffs = item.diff.fileDiffs.map((fileDiff) => {
return {
oldPath: fileDiff.oldPath,
newPath: fileDiff.newPath,
hunks: fileDiff.hunks ? fileDiff.hunks.map((hunk) => ({
oldStart: hunk.oldRange.start,
oldLines: hunk.oldRange.lines,
newStart: hunk.newRange.start,
newLines: hunk.newRange.lines,
body: hunk.body
})) : []
};
});
repositories[repoName].diffs.push({
commitInfo: {
oid: commit.oid,
message: commit.message,
author: commit.author.person.name,
date: commit.author.date
},
fileDiffs
});
}
}
});
// Generate enhanced formatted response
return generateEnhancedResponse(repositories, { query, type, matchCount });
}
exports.formatSearchResultsEnhanced = formatSearchResultsEnhanced;
/**
* Generate an enhanced response with better formatting and code blocks
*/
function generateEnhancedResponse(repositories, queryInfo) {
const { query, type, matchCount } = queryInfo;
const repoNames = Object.keys(repositories);
// Create summary line with Markdown formatting
let response = `## Search Results
Found **${matchCount} matches** across ${repoNames.length} repositories for query: \`${query}\`\n\n`;
// Generate structured response by repository
repoNames.forEach((repoName, index) => {
const repo = repositories[repoName];
response += `### ${index + 1}. Repository: ${repoName}\n\n`;
// Handle file matches with syntax highlighting
if (type === 'file') {
const fileNames = Object.keys(repo.files);
if (fileNames.length > 0) {
fileNames.forEach(fileName => {
// Determine the language for syntax highlighting
const fileExtension = fileName.split('.').pop() || '';
const language = determineLanguage(fileExtension);
response += `#### ${fileName}\n\n`;
// Group matches by line number proximity for better context
const matches = repo.files[fileName];
const sortedMatches = matches.sort((a, b) => a.lineNumber - b.lineNumber);
// Create code blocks with line numbers
let currentBlock = [];
let lastLineNumber = -1;
for (const match of sortedMatches) {
// If there's a gap of more than 3 lines, start a new block
if (lastLineNumber !== -1 && match.lineNumber > lastLineNumber + 3) {
// Output the current block
if (currentBlock.length > 0) {
response += formatCodeBlock(currentBlock, language);
currentBlock = [];
}
}
currentBlock.push({
lineNumber: match.lineNumber,
text: match.preview
});
lastLineNumber = match.lineNumber;
}
// Output any remaining block
if (currentBlock.length > 0) {
response += formatCodeBlock(currentBlock, language);
}
response += '\n';
});
}
}
// Handle commit matches with better formatting
else if (type === 'commit' && repo.commits && repo.commits.length > 0) {
repo.commits.forEach(commit => {
const shortId = commit.oid.substring(0, 7);
response += `#### Commit ${shortId}\n\n`;
response += `**Author:** ${commit.author}${commit.email ? ` <${commit.email}>` : ''}\n\n`;
response += `**Date:** ${formatDate(commit.date)}\n\n`;
// Format commit message as a quote block
const messageLines = commit.message.trim().split('\n');
response += `**Message:**\n\n> ${messageLines.join('\n> ')}\n\n`;
response += `---\n\n`;
});
}
// Handle diff matches with syntax highlighting
else if (type === 'diff' && repo.diffs && repo.diffs.length > 0) {
repo.diffs.forEach(diff => {
const shortId = diff.commitInfo.oid.substring(0, 7);
response += `#### Changes in commit ${shortId}\n\n`;
response += `**Author:** ${diff.commitInfo.author}\n\n`;
response += `**Date:** ${formatDate(diff.commitInfo.date)}\n\n`;
// Format commit message as a quote block
const messageLines = diff.commitInfo.message.trim().split('\n');
response += `**Message:**\n\n> ${messageLines.join('\n> ')}\n\n`;
diff.fileDiffs.forEach(fileDiff => {
const filePath = fileDiff.newPath || fileDiff.oldPath || 'Unknown file';
const fileExtension = filePath.split('.').pop() || '';
const language = determineLanguage(fileExtension);
response += `##### ${filePath}\n\n`;
// Show diffs with syntax highlighting
fileDiff.hunks.forEach(hunk => {
response += `\`\`\`diff\n@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n${hunk.body}\n\`\`\`\n\n`;
});
});
response += `---\n\n`;
});
}
});
// Add a marker at the top indicating this is markdown content
return `<!-- markdown content -->
${response}`;
}
/**
* Format a block of code lines with line numbers
*/
function formatCodeBlock(lines, language) {
let codeBlock = `\`\`\`${language}\n`;
lines.forEach(line => {
codeBlock += `${line.lineNumber}: ${line.text}\n`;
});
codeBlock += '\`\`\`\n\n';
return codeBlock;
}
/**
* Determine the language for syntax highlighting based on file extension
*/
function determineLanguage(extension) {
const languageMap = {
'js': 'javascript',
'ts': 'typescript',
'jsx': 'jsx',
'tsx': 'tsx',
'py': 'python',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'h': 'c',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rb': 'ruby',
'php': 'php',
'html': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'json': 'json',
'md': 'markdown',
'sh': 'bash',
'bash': 'bash',
'yaml': 'yaml',
'yml': 'yaml',
'xml': 'xml',
'sql': 'sql',
'rs': 'rust',
'swift': 'swift',
'kt': 'kotlin',
'scala': 'scala',
'pl': 'perl',
'ex': 'elixir',
'exs': 'elixir',
'erl': 'erlang',
'hs': 'haskell',
'fs': 'fsharp',
'r': 'r'
};
return languageMap[extension.toLowerCase()] || 'text';
}
/**
* Format a date string to a more readable format
*/
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
catch (e) {
return dateString; // Return original if parsing fails
}
}
/**
* Enhanced version of analysis functions for natural language queries
*/
const llm_1 = require("../services/llm");
/**
* Analyze a natural language query to determine search type and parameters
* with enhanced formatting of the results
*/
async function analyzeQueryEnhanced(query) {
try {
// Use LLM to convert the natural query to Sourcegraph syntax
const sourcegraphQuery = await (0, llm_1.convertQueryToSourcegraphSyntax)(query);
console.log(`Converted query: ${sourcegraphQuery}`);
// Parse the Sourcegraph query to extract components
return parseSourcegraphQuery(sourcegraphQuery, query);
}
catch (error) {
console.error('Error during query analysis with LLM:', error);
// Fall back to the rule-based parsing if LLM fails
return parseWithRules(query);
}
}
exports.analyzeQueryEnhanced = analyzeQueryEnhanced;
/**
* Parse a Sourcegraph syntax query to extract components
*/
function parseSourcegraphQuery(sourcegraphQuery, originalQuery) {
// Default parameters
let searchType = 'file';
let searchQuery = sourcegraphQuery;
let author = undefined;
let after = undefined;
const repos = [];
// Extract search type
const typeMatch = sourcegraphQuery.match(/\btype:(\w+)\b/);
if (typeMatch) {
searchType = typeMatch[1];
// Remove the type: parameter from the search query
searchQuery = searchQuery.replace(/\btype:\w+\b/, '').trim();
}
// Extract author
const authorMatch = sourcegraphQuery.match(/\bauthor:([\w.-]+)\b/);
if (authorMatch) {
author = authorMatch[1];
// Remove the author: parameter from the search query
searchQuery = searchQuery.replace(/\bauthor:[\w.-]+\b/, '').trim();
}
// Extract date/after
const afterMatch = sourcegraphQuery.match(/\bafter:(["'][^"']+["']|\S+)\b/);
if (afterMatch) {
after = afterMatch[1].replace(/["']/g, '');
// Remove the after: parameter from the search query
searchQuery = searchQuery.replace(/\bafter:["'][^"']+["']|\bafter:\S+\b/, '').trim();
}
// Extract repositories
const repoRegex = /\brepo:(["'][^"']+["']|\S+)\b/g;
let repoMatch;
while ((repoMatch = repoRegex.exec(sourcegraphQuery)) !== null) {
const repoName = repoMatch[1].replace(/["']/g, '');
repos.push(repoName);
// We don't remove repo: from searchQuery as it's often needed in the final query
}
// Clean up any extra spaces
searchQuery = searchQuery.replace(/\s+/g, ' ').trim();
return {
type: searchType,
query: searchQuery,
author,
after,
repos,
originalQuery // Keep the original query for reference
};
}
/**
* Legacy rule-based parsing as fallback
*/
function parseWithRules(query) {
// Default parameters
let searchType = 'file';
let searchQuery = query;
let author = undefined;
let after = undefined;
const repos = [];
// Extract search type
if (/\b(commit|commits)\b/i.test(query)) {
searchType = 'commit';
}
else if (/\b(diff|diffs|change|changes|pr|prs|pull request|pull requests)\b/i.test(query)) {
searchType = 'diff';
}
// Extract author
const authorMatch = query.match(/\b(?:by|from|author)\s+([\w.-]+)\b/i);
if (authorMatch) {
author = authorMatch[1];
}
// Extract date
const dateMatch = query.match(/\b(?:after|since)\s+(\d{4}-\d{2}-\d{2})\b/i);
if (dateMatch) {
after = dateMatch[1];
}
// Extract repositories
const repoMatches = query.match(/\bin\s+repo(?:sitory)?\s+([\w\/.-]+)\b/ig);
if (repoMatches) {
repoMatches.forEach(match => {
const repoName = match.replace(/\bin\s+repo(?:sitory)?\s+/i, '');
repos.push(repoName);
});
}
// Clean up the search query (remove extracted metadata)
if (author) {
searchQuery = searchQuery.replace(new RegExp(`\\b(?:by|from|author)\\s+${author}\\b`, 'i'), '');
}
if (after) {
searchQuery = searchQuery.replace(new RegExp(`\\b(?:after|since)\\s+${after}\\b`, 'i'), '');
}
if (repos.length > 0) {
repos.forEach(repo => {
searchQuery = searchQuery.replace(new RegExp(`\\bin\\s+repo(?:sitory)?\\s+${repo}\\b`, 'i'), '');
});
}
// Remove search type indicators from the query
searchQuery = searchQuery
.replace(/\b(search for|find|look for)\b/i, '')
.replace(/\b(commit|commits|diff|diffs|change|changes|pr|prs|pull request|pull requests)\b/i, '')
.replace(/\s+/g, ' ')
.trim();
return {
type: searchType,
query: searchQuery,
author,
after,
repos,
originalQuery: query // Keep the original query for reference
};
}