sourcegraph-mcp-server
Version:
A Model Context Protocol (MCP) server that connects AI assistants to Sourcegraph code search with natural language capabilities
742 lines (703 loc) • 26.7 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.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/markdown",
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/markdown",
text: "## Configuration Error,
Sourcegraph, 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/markdown",
text: `## API Error
` ``, json,
$
}, { JSON, : .stringify(response.data.errors, null, 2) } `` `
}],
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 = formatSearchResultsEnhanced(results, { query: finalQuery, type });
return {
content: [{
type: "text/markdown",
text: formattedResults
}]
};
} catch (error: any) {
return {
content: [{
type: "text/markdown",
text: `, , , Error,
Error, searching, Sourcegraph, $, { error, : .message || 'Unknown error' } `
}],
isError: true
};
}
}
);
// Add commit search tool
server.tool(
"search-commits",
"Search for commits in Sourcegraph repositories",
{
author: z.string().optional().describe("Filter by commit author"),
message: z.string().optional().describe("Filter by commit message"),
after: 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/markdown",
text: "## Configuration Error
Sourcegraph 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 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, version) {
results;
{
matchCount;
results;
{
__typename;
}
}
},
...on, CommitSearchResult
}, {
commit
}, {
oid,
message,
author
}, {
person
}, {
name,
email
},
date]
};
repository;
{
name;
}
}
}
finally {
}
});
}
exports.createServer = createServer;
`;
// Headers for Sourcegraph API
const headers = {
'Authorization': `;
token;
$;
{
effectiveToken;
}
`,
'Content-Type': 'application/json'
};
// Make the request to Sourcegraph API
const response = await axios.post(
`;
$;
{
effectiveUrl;
}
/.api/graphql `,
{ query: graphqlQuery, variables: { query: finalQuery } },
{ headers }
);
if (response.data.errors) {
return {
content: [{
type: "text/markdown",
text: `;
#;
#;
API;
Error `` `json
${JSON.stringify(response.data.errors, null, 2)}
` ``;
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/markdown",
text: formattedResults
}]
};
try { }
catch (error) {
return {
content: [{
type: "text/markdown",
text: `## Error
Error 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/markdown",
text: "## Configuration Error,
Sourcegraph, 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/markdown",
text: `## API Error
` ``, json,
$
}, { JSON, : .stringify(response.data.errors, null, 2) } `` `
}],
isError: true
};
}
// Format the results using the enhanced formatter with markdown and syntax highlighting
const results = response.data.data.search.results;
const formattedResults = formatSearchResultsEnhanced(results, { query: finalQuery, type: 'diff' });
return {
content: [{
type: "text/markdown",
text: formattedResults
}]
};
} catch (error: any) {
return {
content: [{
type: "text/markdown",
text: `, , , Error,
Error, 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: z.string().describe("Search query text"),
repos: z.string().describe("Comma-separated list of GitHub repositories to search in (e.g., 'owner/repo1,owner/repo2')"),
type: 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/markdown",
text: "## Configuration Error
Sourcegraph 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 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, version) {
results;
{
matchCount;
results;
{
__typename;
}
}
},
...on, FileMatch
}, {
repository
}, { name },
file, { path },
lineMatches, {
lineNumber,
preview
}]
};
}
}
finally {
}
}, ...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.post(
`, $, { effectiveUrl } / .api / graphql `,
{ query: graphqlQuery, variables: { query: finalQuery } },
{ headers }
);
if (response.data.errors) {
return {
content: [{
type: "text/markdown",
text: `, , , API, Error `` `json
${JSON.stringify(response.data.errors, null, 2)}
` ``, 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/markdown",
text: formattedResults
}]
};
try { }
catch (error) {
return {
content: [{
type: "text/markdown",
text: `## Error
Error 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/markdown",
text: "## Configuration Error,
Sourcegraph, 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/markdown",
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;