mcp-server.ts•7.85 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { DuckDuckGoSearcher } from './search.js';
import { GitHubCodeSearcher } from './github-code-search.js';
import { ContentExtractor } from './extractor.js';
export class MCPResearchServer {
private server: Server;
private searcher: DuckDuckGoSearcher;
private githubSearcher: GitHubCodeSearcher;
private extractor: ContentExtractor;
constructor() {
this.server = new Server(
{
name: 'light-research-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.searcher = new DuckDuckGoSearcher();
this.githubSearcher = new GitHubCodeSearcher();
this.extractor = new ContentExtractor();
this.setupHandlers();
}
private async performSearch(
searcher: DuckDuckGoSearcher | GitHubCodeSearcher,
query: string,
next?: string,
locale?: string
) {
const options = locale ? { locale } : undefined;
const searchResult = await searcher.search(query, next, options);
const results = searchResult.results.map(result => ({
title: result.title,
url: result.url,
snippet: result.snippet,
}));
const response = {
query,
results,
pagination: {
currentPage: searchResult.currentPage,
totalPages: searchResult.totalPages,
totalResults: searchResult.totalResults,
hasNextPage: searchResult.hasNextPage,
hasPreviousPage: searchResult.hasPreviousPage,
nextPageToken: this.getNextPageToken(searcher, searchResult),
},
};
return {
content: [
{
type: 'text',
text: JSON.stringify(response),
},
],
};
}
private getNextPageToken(
searcher: DuckDuckGoSearcher | GitHubCodeSearcher,
searchResult: any
): string | null {
if (!searchResult.hasNextPage) return null;
if (searcher instanceof GitHubCodeSearcher) {
return (searchResult.currentPage + 1).toString();
} else {
return searchResult.nextPageParams ? JSON.stringify(searchResult.nextPageParams) : null;
}
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'github_code_search',
description: 'Search GitHub source code files to research implementation approaches before writing code. Restrictions: Must include search terms (not just "language:js"), only source files <384KB, active repos only. Supports qualifiers: language:LANG, extension:EXT, filename:NAME, path:DIR, user:USER, org:ORG, repo:USER/REPO',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for GitHub source code. Must include search terms, not just qualifiers. Example: "useState language:javascript" or "error handling extension:go"',
},
next: {
type: 'string',
description: 'Next page token from previous search results to get more results',
},
},
required: ['query'],
},
},
{
name: 'duckduckgo_web_search',
description: 'Search the web to research frameworks, libraries, and technologies. Useful for checking if approaches are current, finding documentation, and broad research on development topics. Supports operators: "exact phrase", -exclude, +include, site:domain.com, filetype:pdf, intitle:term, inurl:term',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for web content',
},
next: {
type: 'string',
description: 'Next page token from previous search results to get more results',
},
locale: {
type: 'string',
enum: ['wt-wt', 'us-en', 'uk-en', 'jp-jp', 'cn-zh'],
description: 'DuckDuckGo locale for region-specific search results (default: wt-wt for No region)',
default: 'wt-wt',
},
},
required: ['query'],
},
},
{
name: 'extract_content',
description: 'Extract detailed content from a URL. IMPORTANT: This tool must ONLY be used with URLs obtained from the search results of github_code_search or duckduckgo_web_search tools in this MCP server. Do not use with arbitrary external URLs.',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to extract content from. Must be a URL from previous search results obtained through this MCP server\'s search tools (github_code_search or duckduckgo_web_search).',
},
},
required: ['url'],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'github_code_search':
return await this.handleGitHubSearch(args);
case 'duckduckgo_web_search':
return await this.handleWebSearch(args);
case 'extract_content':
return await this.handleContentExtraction(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
}
private async handleGitHubSearch(args: any) {
const { query, next } = args;
try {
return await this.performSearch(this.githubSearcher, query, next);
} catch (error) {
throw new Error(`GitHub search failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async handleWebSearch(args: any) {
const { query, next, locale = 'wt-wt' } = args;
try {
return await this.performSearch(this.searcher, query, next, locale);
} catch (error) {
throw new Error(`Web search failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async handleContentExtraction(args: any) {
const { url } = args;
try {
const extractedContent = await this.extractor.extract(url);
const response = {
url: extractedContent.url,
title: extractedContent.title,
extractedAt: extractedContent.extractedAt,
content: extractedContent.content,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
} catch (error) {
throw new Error(`Content extraction failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('MCP Research Server running on stdio');
}
async close(): Promise<void> {
await this.server.close();
await this.extractor.close();
}
}