Code Research MCP Server
by nahmanmate
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import * as cheerio from 'cheerio';
import NodeCache from 'node-cache';
// Cache results for 1 hour
const cache = new NodeCache({ stdTTL: 3600 });
// Optional GitHub token for higher rate limits
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
class CodeResearchServer {
private server: Server;
private axiosInstance;
private githubInstance;
constructor() {
this.server = new Server(
{
name: 'code-research-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.axiosInstance = axios.create({
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; CodeResearchBot/1.0)',
},
});
this.githubInstance = axios.create({
baseURL: 'https://api.github.com',
headers: {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'CodeResearchBot/1.0',
...(GITHUB_TOKEN && { 'Authorization': `token ${GITHUB_TOKEN}` })
}
});
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private async searchStackOverflow(query: string, limit: number = 5): Promise<string> {
const cacheKey = `stackoverflow:${query}:${limit}`;
const cached = cache.get<string>(cacheKey);
if (cached) return cached;
try {
const response = await this.axiosInstance.get(
`https://api.stackexchange.com/2.3/search/advanced`,
{
params: {
q: query,
site: 'stackoverflow',
pagesize: limit,
order: 'desc',
sort: 'votes',
filter: 'withbody'
}
}
);
const results = response.data.items.map((item: any) => {
const $ = cheerio.load(item.body);
return {
title: item.title,
link: item.link,
score: item.score,
answer_count: item.answer_count,
excerpt: $.text().substring(0, 200) + '...'
};
});
const formatted = results.map((r: any, i: number) =>
`${i + 1}. ${r.title}\n Score: ${r.score} | Answers: ${r.answer_count}\n ${r.link}\n ${r.excerpt}\n`
).join('\n');
cache.set(cacheKey, formatted);
return formatted;
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Stack Overflow API error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
private async searchMDN(query: string): Promise<string> {
const cacheKey = `mdn:${query}`;
const cached = cache.get<string>(cacheKey);
if (cached) return cached;
try {
const response = await this.axiosInstance.get(
'https://developer.mozilla.org/api/v1/search',
{
params: {
q: query,
locale: 'en-US'
}
}
);
const results = response.data.documents.slice(0, 5).map((doc: any, i: number) =>
`${i + 1}. ${doc.title}\n ${doc.summary}\n https://developer.mozilla.org${doc.mdn_url}\n`
).join('\n');
cache.set(cacheKey, results);
return results;
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`MDN API error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
private async searchGitHub(query: string, language?: string, limit: number = 5): Promise<string> {
const cacheKey = `github:${query}:${language}:${limit}`;
const cached = cache.get<string>(cacheKey);
if (cached) return cached;
try {
// Build search query with language filter if specified
const q = language ? `${query} language:${language}` : query;
// If GitHub token is invalid, fall back to unauthenticated requests
const makeRequest = async (endpoint: string, params: any) => {
try {
const response = await this.githubInstance.get(endpoint, { params });
return response;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Retry without auth token
const response = await this.axiosInstance.get(`https://api.github.com${endpoint}`, {
params,
headers: {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'CodeResearchBot/1.0'
}
});
return response;
}
throw error;
}
};
const [reposResponse, codeResponse] = await Promise.all([
makeRequest('/search/repositories', {
q,
sort: 'stars',
order: 'desc',
per_page: limit
}),
makeRequest('/search/code', {
q,
sort: 'indexed',
order: 'desc',
per_page: limit
})
]);
let result = '=== Top Repositories ===\n';
result += reposResponse.data.items.map((repo: any, i: number) =>
`${i + 1}. ${repo.full_name} (⭐ ${repo.stargazers_count})\n` +
` ${repo.description || 'No description'}\n` +
` ${repo.html_url}\n`
).join('\n');
result += '\n=== Relevant Code ===\n';
result += codeResponse.data.items.map((item: any, i: number) =>
`${i + 1}. ${item.name} (${item.repository.full_name})\n` +
` Path: ${item.path}\n` +
` ${item.html_url}\n`
).join('\n');
cache.set(cacheKey, result);
return result;
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`GitHub API error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
private async searchNpm(query: string, limit: number = 5): Promise<string> {
const cacheKey = `npm:${query}:${limit}`;
const cached = cache.get<string>(cacheKey);
if (cached) return cached;
try {
const response = await this.axiosInstance.get(
`https://registry.npmjs.org/-/v1/search`,
{
params: {
text: query,
size: limit
}
}
);
const results = response.data.objects.map((item: any, i: number) => {
const pkg = item.package;
return `${i + 1}. ${pkg.name} (v${pkg.version})\n` +
` ${pkg.description || 'No description'}\n` +
` Weekly Downloads: ${pkg.downloads}\n` +
` ${pkg.links.npm}\n`;
}).join('\n');
cache.set(cacheKey, results);
return results;
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`npm API error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
private async searchPyPI(query: string, limit: number = 5): Promise<string> {
const cacheKey = `pypi:${query}:${limit}`;
const cached = cache.get<string>(cacheKey);
if (cached) return cached;
try {
const response = await this.axiosInstance.get(
`https://pypi.org/pypi/${encodeURIComponent(query)}/json`
);
const pkg = response.data.info;
const result = `Package: ${pkg.name} (v${pkg.version})\n` +
`Description: ${pkg.summary || 'No description'}\n` +
`Author: ${pkg.author || 'Unknown'}\n` +
`Homepage: ${pkg.home_page || pkg.project_url || 'N/A'}\n` +
`PyPI: https://pypi.org/project/${pkg.name}/\n`;
cache.set(cacheKey, result);
return result;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return `No package found for "${query}"`;
}
throw new McpError(
ErrorCode.InternalError,
`PyPI API error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
private async searchAll(query: string, limit: number = 3): Promise<string> {
const cacheKey = `all:${query}:${limit}`;
const cached = cache.get<string>(cacheKey);
if (cached) return cached;
try {
// Execute non-GitHub searches first
const [so, mdn, npm, pypi] = await Promise.all([
this.searchStackOverflow(query, limit).catch(error =>
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`
),
this.searchMDN(query).catch(error =>
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`
),
this.searchNpm(query, limit).catch(error =>
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`
),
this.searchPyPI(query).catch(error =>
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`
)
]);
let results = `=== Stack Overflow Results ===\n${so}\n\n` +
`=== MDN Documentation ===\n${mdn}\n\n`;
// Try GitHub search separately
try {
const gh = await this.searchGitHub(query, undefined, limit);
results += `=== GitHub Results ===\n${gh}\n\n`;
} catch (error) {
results += `=== GitHub Results ===\nGitHub search currently unavailable\n\n`;
}
results += `=== npm Packages ===\n${npm}\n\n` +
`=== PyPI Packages ===\n${pypi}`;
cache.set(cacheKey, results);
return results;
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Search all platforms error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'search_stackoverflow',
description: 'Search Stack Overflow for programming questions and answers',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Maximum number of results (default: 5)',
minimum: 1,
maximum: 10
}
},
required: ['query']
}
},
{
name: 'search_mdn',
description: 'Search MDN Web Docs for web development documentation',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
}
},
required: ['query']
}
},
{
name: 'search_github',
description: 'Search GitHub for repositories and code',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
language: {
type: 'string',
description: 'Filter by programming language'
},
limit: {
type: 'number',
description: 'Maximum number of results per category (default: 5)',
minimum: 1,
maximum: 10
}
},
required: ['query']
}
},
{
name: 'search_npm',
description: 'Search npm registry for JavaScript packages',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Maximum number of results (default: 5)',
minimum: 1,
maximum: 10
}
},
required: ['query']
}
},
{
name: 'search_pypi',
description: 'Search PyPI for Python packages',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
}
},
required: ['query']
}
},
{
name: 'search_all',
description: 'Search all platforms simultaneously',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Maximum results per platform (1-5, default: 3)',
minimum: 1,
maximum: 5
}
},
required: ['query']
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'search_stackoverflow': {
const { query, limit } = request.params.arguments as { query: string; limit?: number };
const results = await this.searchStackOverflow(query, limit);
return {
content: [
{
type: 'text',
text: results
}
]
};
}
case 'search_mdn': {
const { query } = request.params.arguments as { query: string };
const results = await this.searchMDN(query);
return {
content: [
{
type: 'text',
text: results
}
]
};
}
case 'search_github': {
const { query, language, limit } = request.params.arguments as {
query: string;
language?: string;
limit?: number;
};
const results = await this.searchGitHub(query, language, limit);
return {
content: [
{
type: 'text',
text: results
}
]
};
}
case 'search_npm': {
const { query, limit } = request.params.arguments as { query: string; limit?: number };
const results = await this.searchNpm(query, limit);
return {
content: [
{
type: 'text',
text: results
}
]
};
}
case 'search_pypi': {
const { query } = request.params.arguments as { query: string };
const results = await this.searchPyPI(query);
return {
content: [
{
type: 'text',
text: results
}
]
};
}
case 'search_all': {
const { query, limit } = request.params.arguments as { query: string; limit?: number };
const results = await this.searchAll(query, limit);
return {
content: [
{
type: 'text',
text: results
}
]
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Code Research MCP server running on stdio');
}
}
const server = new CodeResearchServer();
server.run().catch(console.error);