sourcegraph-mcp-server
Version:
A Model Context Protocol (MCP) server that connects AI assistants to Sourcegraph code search with natural language capabilities
391 lines (390 loc) • 16.7 kB
JavaScript
;
/**
* Deep Code Research Service
* Handles deep code research functionality separate from the main MCP server
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.performDeepCodeResearch = void 0;
const axios_1 = __importDefault(require("axios"));
/**
* Perform deep code research on a given query
*/
async function performDeepCodeResearch(params) {
const { query, repo, language, limit = 20, url, token } = params;
try {
console.error('Deep research service called with params:', { query, repo, language, limit });
// Build the search query
let searchQuery = query;
// Add repository filter if specified
if (repo) {
if (!repo.includes('/')) {
// If repo doesn't contain '/', assume it's a GitHub username/org
searchQuery += ` repo:^github\\.com/${repo}/`;
}
else if (!repo.includes('.')) {
// If repo has '/' but no domain, assume GitHub
searchQuery += ` repo:^github\\.com/${repo}$`;
}
else {
// Full domain specified
searchQuery += ` repo:${repo}`;
}
}
// Add language filter if specified
if (language) {
searchQuery += ` lang:${language}`;
}
// Add count parameter
searchQuery += ` count:${limit}`;
console.error('Constructed search query:', searchQuery);
// Headers for Sourcegraph API
const headers = {
'Authorization': `token ${token}`,
'Content-Type': 'application/json'
};
// 1. First search: File search
const fileQuery = searchQuery + ' type:file';
const fileSearchQuery = `
query FileSearch($query: String!) {
search(query: $query, version: V3) {
results {
matchCount
results {
__typename
... on FileMatch {
repository { name }
file { path }
lineMatches {
lineNumber
preview
}
}
}
}
}
}
`;
console.error('Executing file search with query:', fileQuery);
const fileResponse = await axios_1.default.post(`${url}/.api/graphql`, { query: fileSearchQuery, variables: { query: fileQuery } }, { headers });
// 2. Second search: Commit search
const commitQuery = searchQuery + ' type:commit';
const commitSearchQuery = `
query CommitSearch($query: String!) {
search(query: $query, version: V3) {
results {
matchCount
results {
__typename
... on CommitSearchResult {
commit {
oid
message
author {
person {
name
email
}
date
}
repository { name }
}
}
}
}
}
}
`;
console.error('Executing commit search with query:', commitQuery);
const commitResponse = await axios_1.default.post(`${url}/.api/graphql`, { query: commitSearchQuery, variables: { query: commitQuery } }, { headers });
// Format results
let resultsText = `## Deep Code Research: ${query}\n\n`;
// Process file search results
const fileResults = fileResponse.data.data.search.results;
console.error(`File search found ${fileResults.matchCount} matches`);
if (fileResults.matchCount > 0 && fileResults.results.length > 0) {
resultsText += `### Code Findings\n`;
resultsText += `Found ${fileResults.matchCount} code matches.\n\n`;
// Group results by repository
const repoMap = {};
fileResults.results.forEach(item => {
if (item.__typename === 'FileMatch') {
const repoName = item.repository.name;
const filePath = item.file.path;
if (!repoMap[repoName]) {
repoMap[repoName] = {};
}
if (!repoMap[repoName][filePath]) {
repoMap[repoName][filePath] = [];
}
item.lineMatches.forEach(match => {
repoMap[repoName][filePath].push({
lineNumber: match.lineNumber,
preview: match.preview
});
});
}
});
// Format results by repository
Object.keys(repoMap).forEach((repoName, repoIndex) => {
resultsText += `${repoIndex + 1}. **${repoName}**:\n\n`;
Object.keys(repoMap[repoName]).forEach(filePath => {
resultsText += ` * **${filePath}**:\n`;
repoMap[repoName][filePath].slice(0, 5).forEach(match => {
resultsText += ` Line ${match.lineNumber}: \`${escapeMarkdown(match.preview)}\`\n`;
});
if (repoMap[repoName][filePath].length > 5) {
resultsText += ` ... and ${repoMap[repoName][filePath].length - 5} more matches\n`;
}
resultsText += '\n';
});
});
// Add code insights
resultsText += "### Code Patterns & Insights\n";
// Analyze file types and directories
const fileTypes = {};
const directories = {};
fileResults.results.forEach(item => {
if (item.__typename === 'FileMatch') {
const filePath = item.file.path;
// Track file extensions
const fileExt = filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase();
fileTypes[fileExt] = (fileTypes[fileExt] || 0) + 1;
// Track directories
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
if (dirPath) {
directories[dirPath] = (directories[dirPath] || 0) + 1;
}
}
});
// List prominent files
resultsText += "- Key files containing this functionality:\n";
fileResults.results
.filter(item => item.__typename === 'FileMatch' && item.lineMatches.length > 2)
.slice(0, 5)
.forEach(item => {
resultsText += ` - \`${item.file.path}\` (${item.lineMatches.length} matches)\n`;
});
// Add file type statistics
resultsText += "\n- File type distribution:\n";
Object.entries(fileTypes)
.filter(([_, count]) => count > 1)
.forEach(([ext, count]) => {
resultsText += ` - ${ext}: ${count} files\n`;
});
// Add directory statistics
resultsText += "\n- Key directories:\n";
Object.entries(directories)
.sort(([_, countA], [__, countB]) => countB - countA)
.slice(0, 5)
.filter(([dir, count]) => count > 1 && dir)
.forEach(([dir, count]) => {
resultsText += ` - ${dir}/: ${count} files\n`;
});
}
else {
resultsText += "### Code Findings\nNo code matches found.\n\n";
}
// Process commit search results
const commitResults = commitResponse.data.data.search.results;
console.error(`Commit search found ${commitResults.matchCount} matches`);
if (commitResults.matchCount > 0 && commitResults.results.length > 0) {
resultsText += `\n### Related Commits\n`;
resultsText += `Found ${commitResults.matchCount} related commits.\n\n`;
// Group results by repository
const repoMap = {};
commitResults.results.forEach(item => {
if (item.__typename === 'CommitSearchResult' && item.commit) {
const repoName = item.commit.repository.name;
if (!repoMap[repoName]) {
repoMap[repoName] = [];
}
repoMap[repoName].push({
oid: item.commit.oid,
message: item.commit.message,
author: item.commit.author.person ? item.commit.author.person.name : 'Unknown',
date: item.commit.author.date
});
}
});
// Format results by repository
Object.keys(repoMap).forEach((repoName, repoIndex) => {
resultsText += `${repoIndex + 1}. **${repoName}**:\n\n`;
repoMap[repoName].forEach(commit => {
const shortId = commit.oid.substring(0, 7);
resultsText += ` * Commit ${shortId} by ${commit.author}\n`;
resultsText += ` Date: ${formatDate(commit.date)}\n`;
resultsText += ` Message: ${escapeMarkdown(commit.message.trim())}\n\n`;
});
});
// Add commit insights
resultsText += "### Development Insights\n";
// Track authors and dates
const authors = {};
const datePattern = {};
commitResults.results.forEach(item => {
if (item.__typename === 'CommitSearchResult' && item.commit) {
// Track commit authors
if (item.commit.author && item.commit.author.person) {
const author = item.commit.author.person.name || 'Unknown';
authors[author] = (authors[author] || 0) + 1;
}
// Track commit dates
if (item.commit.author && item.commit.author.date) {
const date = new Date(item.commit.author.date);
const month = date.toLocaleString('default', { month: 'short', year: 'numeric' });
datePattern[month] = (datePattern[month] || 0) + 1;
}
}
});
// Add author statistics
resultsText += "- Main contributors:\n";
Object.entries(authors)
.sort(([_, countA], [__, countB]) => countB - countA)
.slice(0, 5)
.forEach(([author, count]) => {
resultsText += ` - ${author}: ${count} commits\n`;
});
// Add date pattern
resultsText += "\n- Development timeline:\n";
Object.entries(datePattern)
.sort() // Sort chronologically
.forEach(([month, count]) => {
resultsText += ` - ${month}: ${count} commits\n`;
});
}
else {
resultsText += "\n### Related Commits\nNo related commits found.\n";
}
// Prepare structured data for agent consumption
const structuredData = {
query,
repo: repo || 'all repositories',
language: language || 'all languages',
summary: {
codeMatchCount: fileResults?.matchCount || 0,
commitMatchCount: commitResults?.matchCount || 0,
repositoriesFound: fileResults?.matchCount > 0 ?
Object.keys(fileResults.results.reduce((repos, item) => {
if (item.__typename === 'FileMatch') {
repos[item.repository.name] = true;
}
return repos;
}, {})).length : 0
},
codeFindings: fileResults?.matchCount > 0 ? fileResults.results
.filter(item => item.__typename === 'FileMatch')
.map(item => ({
repository: item.repository.name,
path: item.file.path,
matchCount: item.lineMatches.length,
snippets: item.lineMatches.slice(0, 3).map(match => ({
lineNumber: match.lineNumber,
code: match.preview
}))
})) : [],
commits: commitResults?.matchCount > 0 ? commitResults.results
.filter(item => item.__typename === 'CommitSearchResult' && item.commit)
.map(item => ({
repository: item.commit.repository.name,
id: item.commit.oid.substring(0, 7),
message: item.commit.message.trim(),
author: item.commit.author.person ? item.commit.author.person.name : 'Unknown',
date: item.commit.author.date
})) : []
};
// Combine markdown and JSON in a single text response for MCP compatibility
const combinedText = resultsText +
'\n\n---\n\n' +
'## Structured Data (JSON)\n' +
'```json\n' +
JSON.stringify(structuredData, null, 2) +
'\n```\n';
return {
content: [{
type: "text",
text: combinedText
}]
};
}
catch (error) {
console.error('Deep code researcher service error:', error);
// Build detailed error message
let errorText = `Error in deep code research: ${error.message || 'Unknown error'}`;
errorText += '\n\nDebugging Information:\n';
errorText += `- Query: ${query}\n`;
errorText += `- Repository: ${repo || 'Not specified'}\n`;
errorText += `- Language: ${language || 'Not specified'}\n`;
errorText += `- Limit: ${limit}\n`;
if (error.response) {
errorText += '\nAPI Response Error:\n';
errorText += `- Status: ${error.response.status}\n`;
errorText += `- Message: ${JSON.stringify(error.response.data)}\n`;
}
errorText += '\nPossible solutions:\n';
errorText += '- Verify your Sourcegraph instance is accessible\n';
errorText += '- Check if the specified repository exists\n';
errorText += '- Try a more general query without repository filter\n';
errorText += '- Confirm your Sourcegraph token has proper permissions\n';
// Also include structured error data for agent consumption
const errorData = {
error: true,
message: error.message || 'Unknown error',
query,
parameters: {
repo: repo || null,
language: language || null,
limit
},
responseInfo: error.response ? {
status: error.response.status,
data: error.response.data
} : null,
suggestions: [
'Verify your Sourcegraph instance is accessible',
'Check if the specified repository exists',
'Try a more general query without repository filter',
'Confirm your Sourcegraph token has proper permissions'
]
};
// Combine error text and structured error data in a single response for MCP compatibility
const combinedErrorText = errorText +
'\n\n---\n\n' +
'## Error Details (JSON)\n' +
'```json\n' +
JSON.stringify(errorData, null, 2) +
'\n```\n';
return {
content: [{
type: "text",
text: combinedErrorText
}],
isError: true
};
}
}
exports.performDeepCodeResearch = performDeepCodeResearch;
/**
* Format a date string to a more readable format
*/
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleString();
}
catch (e) {
return dateString; // Return original if parsing fails
}
}
/**
* Escape markdown special characters in text
*/
function escapeMarkdown(text) {
return text
.replace(/\*/g, '\\*')
.replace(/\_/g, '\\_')
.replace(/\~/g, '\\~')
.replace(/\`/g, '\\`');
}