sourcegraph-mcp-server
Version:
A Model Context Protocol (MCP) server that connects AI assistants to Sourcegraph code search with natural language capabilities
659 lines (656 loc) • 27.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createServer = void 0;
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const zod_1 = require("zod");
const dotenv_1 = __importDefault(require("dotenv"));
const axios_1 = __importDefault(require("axios"));
const natural_language_1 = require("./services/natural-language");
const enhanced_formatter_1 = require("./utils/enhanced-formatter");
// Load environment variables
dotenv_1.default.config();
// Sourcegraph API configuration
const sgUrl = process.env.SOURCEGRAPH_URL;
const sgToken = process.env.SOURCEGRAPH_TOKEN;
const test_tools_1 = require("./test-tools");
/**
* Creates and configures the Sourcegraph MCP server
* with resources, prompts, and tools
*/
function createServer() {
const toolImplementations = {};
// Create an MCP server
const server = new mcp_js_1.McpServer({
name: "sourcegraph-mcp-server",
version: "1.0.0",
debug: false, // Disable debug mode in production to prevent console output breaking JSON
});
// Add a static resource
server.resource("hello", "hello://sourcegraph", async (uri) => ({
contents: [
{
uri: uri.href,
text: "Hello from Sourcegraph MCP Server! Ready to search code repositories.",
},
],
}));
// Add a dynamic resource with parameters
server.resource("greeting", new mcp_js_1.ResourceTemplate("greeting://{name}", { list: undefined }), async (uri, { name }) => ({
contents: [
{
uri: uri.href,
text: `Hello, ${name}! Welcome to the Sourcegraph MCP Server.`,
},
],
}));
// Add a prompt
server.prompt("sourcegraph-assistant", "A prompt that introduces Sourcegraph search capabilities", () => ({
messages: [
{
role: "assistant",
content: {
type: "text",
text: "I'm a Sourcegraph assistant that can help you search through code repositories. You can ask me to search for code, commits, or diffs.",
},
},
],
}));
// Add an echo tool
// Just use direct implementation in the tool
toolImplementations["echo"] = async (args) => {
return {
content: [
{
type: "text",
text: `**Hello** ${args.message}`,
},
],
};
};
server.tool("echo", "Echoes back a message with 'Hello' prefix", { message: zod_1.z.string().describe("The message to echo") }, async ({ message }) => ({
content: [
{
type: "text",
text: `**Hello** ${message}`,
},
],
}));
// Add code search tool - now with natural language processing support
server.tool("search-code", "Search for code across Sourcegraph repositories", {
query: zod_1.z.string().describe("Search query text"),
type: zod_1.z.enum(['file', 'commit', 'diff']).default('file').describe("Type of search: file, commit, or diff")
}, async ({ query, type }) => {
// Validate Sourcegraph credentials
const effectiveUrl = sgUrl || process.env.SOURCEGRAPH_URL;
const effectiveToken = sgToken || process.env.SOURCEGRAPH_TOKEN;
if (!effectiveUrl || !effectiveToken) {
return {
content: [{
type: "text",
text: "## Configuration Error\n\nSourcegraph URL or token not configured. Please set SOURCEGRAPH_URL and SOURCEGRAPH_TOKEN environment variables."
}],
isError: true
};
}
try {
// Check if the query seems to be natural language
// If it contains common phrases like "find" or doesn't have specific operators
const looksLikeNaturalLanguage = /^(find|show|get|search for|look for|what|where|how|when)/i.test(query) ||
!/(repo:|type:|lang:|after:|content:|file:|case:|patterntype:)/i.test(query);
let finalQuery;
if (looksLikeNaturalLanguage) {
// Process through natural language query analyzer
try {
// Use the enhanced analyzeQuery function to get a more precise query
const analyzed = await (0, enhanced_formatter_1.analyzeQueryEnhanced)(query);
// Use the suggested type if provided in parameters
const finalType = type || analyzed.type;
finalQuery = `${analyzed.query} type:${finalType} count:20`;
}
catch (nlError) {
console.error('Natural language processing failed, using original query:', nlError);
finalQuery = `${query} type:${type} count:20`;
}
}
else {
// Direct Sourcegraph syntax - just add type if not already present
finalQuery = query.includes('type:') ? query : `${query} type:${type}`;
// Add count if not present
finalQuery = finalQuery.includes('count:') ? finalQuery : `${finalQuery} count:20`;
}
// The GraphQL query
const graphqlQuery = `
query CodeSearch($query: String!) {
search(query: $query, version: V3) {
results {
matchCount
results {
__typename
... on FileMatch {
repository { name }
file { path }
lineMatches {
lineNumber
preview
}
}
... on CommitSearchResult {
commit {
oid
message
author {
person {
name
email
}
date
}
repository { name }
}
}
}
}
}
}
`;
// Headers for Sourcegraph API
const headers = {
'Authorization': `token ${effectiveToken}`,
'Content-Type': 'application/json'
};
// Make the request to Sourcegraph API
const response = await axios_1.default.post(`${effectiveUrl}/.api/graphql`, { query: graphqlQuery, variables: { query: finalQuery } }, { headers });
if (response.data.errors) {
return {
content: [{
type: "text",
text: `## API Error\n\n\`\`\`json\n${JSON.stringify(response.data.errors, null, 2)}\n\`\`\``
}],
isError: true
};
}
// Format the results
const results = response.data.data.search.results;
// Use the enhanced formatter for results with better human-readable output
const formattedResults = (0, enhanced_formatter_1.formatSearchResultsEnhanced)(results, { query: finalQuery, type });
return {
content: [{
type: "text",
text: formattedResults
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `## Error\n\nError searching Sourcegraph: ${error.message || 'Unknown error'}`
}],
isError: true
};
}
});
// Add commit search tool
server.tool("search-commits", "Search for commits in Sourcegraph repositories", {
author: zod_1.z.string().optional().describe("Filter by commit author"),
message: zod_1.z.string().optional().describe("Filter by commit message"),
after: zod_1.z.string().optional().describe("Filter for commits after this date (YYYY-MM-DD)")
}, async ({ author, message, after }) => {
// Validate Sourcegraph credentials
const effectiveUrl = sgUrl || process.env.SOURCEGRAPH_URL;
const effectiveToken = sgToken || process.env.SOURCEGRAPH_TOKEN;
if (!effectiveUrl || !effectiveToken) {
return {
content: [{
type: "text",
text: "## Configuration Error\n\nSourcegraph URL or token not configured. Please set SOURCEGRAPH_URL and SOURCEGRAPH_TOKEN environment variables."
}],
isError: true
};
}
try {
// Check if the message parameter might be a natural language query
let finalQuery;
let nlQuery = message;
// Only attempt NL processing if message looks like natural language
if (nlQuery && /^(find|show|get|search for|look for|what|where|how|when)/i.test(nlQuery)) {
try {
// Use the enhanced analyzeQuery function to get a more precise query
const analyzed = await (0, enhanced_formatter_1.analyzeQueryEnhanced)(nlQuery);
// Build the final query using the analyzed components and any explicit parameters
finalQuery = 'type:commit';
// Add search terms from analysis
if (analyzed.query)
finalQuery += ` ${analyzed.query}`;
// Use explicitly provided parameters if available, otherwise use analyzed ones
const finalAuthor = author || analyzed.author;
const finalAfter = after || analyzed.after;
if (finalAuthor)
finalQuery += ` author:${finalAuthor}`;
if (finalAfter)
finalQuery += ` after:${finalAfter}`;
finalQuery += ' count:20';
}
catch (nlError) {
console.error('Natural language processing failed, using original parameters:', nlError);
// Fall back to standard query building
finalQuery = 'type:commit';
if (author)
finalQuery += ` author:${author}`;
if (message)
finalQuery += ` message:${message}`;
if (after)
finalQuery += ` after:${after}`;
finalQuery += ' count:20';
}
}
else {
// Build the search query using provided parameters directly
finalQuery = 'type:commit';
if (author)
finalQuery += ` author:${author}`;
if (message)
finalQuery += ` message:${message}`;
if (after)
finalQuery += ` after:${after}`;
finalQuery += ' count:20';
}
// The GraphQL query
const graphqlQuery = `
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 }
}
}
}
}
}
}
`;
// Headers for Sourcegraph API
const headers = {
'Authorization': `token ${effectiveToken}`,
'Content-Type': 'application/json'
};
// Make the request to Sourcegraph API
const response = await axios_1.default.post(`${effectiveUrl}/.api/graphql`, { query: graphqlQuery, variables: { query: finalQuery } }, { headers });
if (response.data.errors) {
return {
content: [{
type: "text",
text: `## API Error\n\n\`\`\`json\n${JSON.stringify(response.data.errors, null, 2)}\n\`\`\``
}],
isError: true
};
}
// Format the results using the enhanced formatter with markdown and highlighting
const results = response.data.data.search.results;
const formattedResults = (0, enhanced_formatter_1.formatSearchResultsEnhanced)(results, { query: finalQuery, type: 'commit' });
return {
content: [{
type: "text",
text: formattedResults
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `## Error\n\nError searching Sourcegraph commits: ${error.message || 'Unknown error'}`
}],
isError: true
};
}
});
// Add diff search tool
server.tool("search-diffs", "Search for code changes (diffs) in Sourcegraph repositories", {
query: zod_1.z.string().optional().describe("Search query text"),
author: zod_1.z.string().optional().describe("Filter by commit author"),
after: zod_1.z.string().optional().describe("Filter for diffs after this date (YYYY-MM-DD)")
}, async ({ query, author, after }) => {
// Validate Sourcegraph credentials
const effectiveUrl = sgUrl || process.env.SOURCEGRAPH_URL;
const effectiveToken = sgToken || process.env.SOURCEGRAPH_TOKEN;
if (!effectiveUrl || !effectiveToken) {
return {
content: [{
type: "text",
text: "## Configuration Error\n\nSourcegraph URL or token not configured. Please set SOURCEGRAPH_URL and SOURCEGRAPH_TOKEN environment variables."
}],
isError: true
};
}
try {
// Check if the query looks like natural language
let finalQuery;
if (query && /^(find|show|get|search for|look for|what|where|how|when)/i.test(query)) {
// Process through natural language query analyzer
try {
// Use the enhanced analyzeQuery function to get a more precise query
const analyzed = await (0, enhanced_formatter_1.analyzeQueryEnhanced)(query);
finalQuery = analyzed.query;
// Use explicitly provided parameters if available
const finalAuthor = author || analyzed.author;
const finalAfter = after || analyzed.after;
finalQuery = `type:diff ${finalQuery}`;
if (finalAuthor)
finalQuery += ` author:${finalAuthor}`;
if (finalAfter)
finalQuery += ` after:${finalAfter}`;
finalQuery += ' count:20';
}
catch (nlError) {
console.error('Natural language processing failed, using original query:', nlError);
// Fall back to standard query building
finalQuery = 'type:diff';
if (query)
finalQuery += ` ${query}`;
if (author)
finalQuery += ` author:${author}`;
if (after)
finalQuery += ` after:${after}`;
finalQuery += ' count:20';
}
}
else {
// Build the search query with provided parameters
finalQuery = 'type:diff';
if (query)
finalQuery += ` ${query}`;
if (author)
finalQuery += ` author:${author}`;
if (after)
finalQuery += ` after:${after}`;
finalQuery += ' count:20';
}
// The GraphQL query
const graphqlQuery = `
query DiffSearch($query: String!) {
search(query: $query, version: V3) {
results {
matchCount
results {
__typename
... on CommitSearchResult {
commit {
oid
message
author {
person {
name
email
}
date
}
repository { name }
}
diff {
fileDiffs {
oldPath
newPath
hunks {
body
oldRange { start, lines }
newRange { start, lines }
}
}
}
}
}
}
}
}
`;
// Headers for Sourcegraph API
const headers = {
'Authorization': `token ${effectiveToken}`,
'Content-Type': 'application/json'
};
// Make the request to Sourcegraph API
const response = await axios_1.default.post(`${effectiveUrl}/.api/graphql`, { query: graphqlQuery, variables: { query: finalQuery } }, { headers });
if (response.data.errors) {
return {
content: [{
type: "text",
text: `## API Error\n\n\`\`\`json\n${JSON.stringify(response.data.errors, null, 2)}\n\`\`\``
}],
isError: true
};
}
// Format the results using the enhanced formatter with markdown and syntax highlighting
const results = response.data.data.search.results;
const formattedResults = (0, enhanced_formatter_1.formatSearchResultsEnhanced)(results, { query: finalQuery, type: 'diff' });
return {
content: [{
type: "text",
text: formattedResults
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `## Error\n\nError searching Sourcegraph diffs: ${error.message || 'Unknown error'}`
}],
isError: true
};
}
});
// Add a tool to search specifically in GitHub repositories
server.tool("search-github-repos", "Search for code in specific GitHub repositories", {
query: zod_1.z.string().describe("Search query text"),
repos: zod_1.z.string().describe("Comma-separated list of GitHub repositories to search in (e.g., 'owner/repo1,owner/repo2')"),
type: zod_1.z.enum(['file', 'commit', 'diff']).default('file').describe("Type of search: file, commit, or diff")
}, async ({ query, repos, type }) => {
// Validate Sourcegraph credentials
const effectiveUrl = sgUrl || process.env.SOURCEGRAPH_URL;
const effectiveToken = sgToken || process.env.SOURCEGRAPH_TOKEN;
if (!effectiveUrl || !effectiveToken) {
return {
content: [{
type: "text",
text: "## Configuration Error\n\nSourcegraph URL or token not configured. Please set SOURCEGRAPH_URL and SOURCEGRAPH_TOKEN environment variables."
}],
isError: true
};
}
try {
// Parse the repo list
const repoList = repos.split(',').map(r => r.trim());
// Build the search query with repo filters
const repoFilters = repoList.map(repo => `repo:^github\\.com/${repo}$`).join(' ');
// Check if the query is natural language
let searchTerms = query;
const isNaturalLanguage = /^(find|show|get|search for|look for|what|where|how|when)/i.test(query);
if (isNaturalLanguage) {
try {
// Process through natural language query analyzer
const analyzed = await (0, enhanced_formatter_1.analyzeQueryEnhanced)(query);
searchTerms = analyzed.query;
}
catch (nlError) {
console.error('Natural language processing failed, using original query:', nlError);
}
}
// Build the final search query
const finalQuery = `${searchTerms} ${repoFilters} type:${type} count:20`;
// The GraphQL query
const graphqlQuery = `
query GitHubRepoSearch($query: String!) {
search(query: $query, version: V3) {
results {
matchCount
results {
__typename
... on FileMatch {
repository { name }
file { path }
lineMatches {
lineNumber
preview
}
}
... on CommitSearchResult {
commit {
oid
message
author {
person {
name
email
}
date
}
repository { name }
}
}
}
}
}
}
`;
// Headers for Sourcegraph API
const headers = {
'Authorization': `token ${effectiveToken}`,
'Content-Type': 'application/json'
};
// Make the request to Sourcegraph API
const response = await axios_1.default.post(`${effectiveUrl}/.api/graphql`, { query: graphqlQuery, variables: { query: finalQuery } }, { headers });
if (response.data.errors) {
return {
content: [{
type: "text",
text: `## API Error\n\n\`\`\`json\n${JSON.stringify(response.data.errors, null, 2)}\n\`\`\``
}],
isError: true
};
}
// Format the results using the enhanced formatter with better human-readable display
const results = response.data.data.search.results;
const formattedResults = (0, enhanced_formatter_1.formatSearchResultsEnhanced)(results, { query: finalQuery, type });
return {
content: [{
type: "text",
text: formattedResults
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `## Error\n\nError searching GitHub repositories: ${error.message || 'Unknown error'}`
}],
isError: true
};
}
});
// Add a natural language search tool
server.tool("natural-search", "Search code repositories using natural language queries", {
query: zod_1.z.string().describe("Natural language query describing what you want to search for"),
max_results: zod_1.z.number().optional().describe("Maximum number of results to return (default: 20)")
}, async ({ query, max_results }) => {
// Validate Sourcegraph credentials
const effectiveUrl = sgUrl || process.env.SOURCEGRAPH_URL;
const effectiveToken = sgToken || process.env.SOURCEGRAPH_TOKEN;
if (!effectiveUrl || !effectiveToken) {
return {
content: [{
type: "text",
text: "## Configuration Error\n\nSourcegraph URL or token not configured. Please set SOURCEGRAPH_URL and SOURCEGRAPH_TOKEN environment variables."
}],
isError: true
};
}
const result = await (0, natural_language_1.naturalLanguageSearch)(query, {
url: effectiveUrl,
token: effectiveToken
});
return result;
});
// Add a debug tool to list available tools and methods
server.tool("debug", "Lists all available tools and methods", {}, async () => {
// Create a more human-readable markdown-formatted response
const toolsInfo = {
tools: [
"echo",
"search-code",
"search-commits",
"search-diffs",
"search-github-repos",
"natural-search",
"test-nl-search",
"test-connection",
"nl-search-help",
"debug"
],
resources: [
"hello://sourcegraph",
"greeting://{name}"
],
prompts: [
"sourcegraph-assistant"
],
methods: ["tools/invoke", "mcp/capabilities", "debug/info"]
};
// Format as markdown with sections and lists
let markdownOutput = `## Sourcegraph MCP Server Capabilities
### Available Tools
`;
toolsInfo.tools.forEach(tool => {
markdownOutput += `* \`${tool}\`\n`;
});
markdownOutput += `\n### Available Resources\n\n`;
toolsInfo.resources.forEach(resource => {
markdownOutput += `* \`${resource}\`\n`;
});
markdownOutput += `\n### Available Prompts\n\n`;
toolsInfo.prompts.forEach(prompt => {
markdownOutput += `* \`${prompt}\`\n`;
});
markdownOutput += `\n### Supported Methods\n\n`;
toolsInfo.methods.forEach(method => {
markdownOutput += `* \`${method}\`\n`;
});
markdownOutput += `\n\nAll tools now return results in enhanced human-readable format with markdown and syntax highlighting.`;
return {
content: [
{
type: "text",
text: markdownOutput
},
],
};
});
// Add invoke method to the server for direct tool invocation
server.invoke = async (toolName, params) => {
if (toolName in toolImplementations) {
return await toolImplementations[toolName](params);
}
throw new Error(`Tool '${toolName}' not found`);
};
// Add test tools for easier debugging and validation
(0, test_tools_1.addTestTools)(server);
return server;
}
exports.createServer = createServer;