MCP DuckDuckGo Search Server
by spences10
Verified
#!/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 { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const pkg = JSON.parse(
readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
);
const { name, version } = pkg;
// SerpAPI configuration
const SERPAPI_BASE_URL = 'https://serpapi.com/search.json';
const SERPAPI_KEY = process.env.SERPAPI_API_KEY || '';
if (!SERPAPI_KEY) {
throw new Error('SERPAPI_API_KEY environment variable is required');
}
// Assert SERPAPI_KEY is string since we check for existence
const API_KEY: string = SERPAPI_KEY;
import { SerpApiResponse, CacheEntry, FormattedResponse } from './types.js';
import { ServerResult } from '@modelcontextprotocol/sdk/types.js';
// Import cache singleton
import { search_cache } from './cache.js';
// Import response formatter
import { format_response } from './formatters.js';
// Import configuration
import { SAFE_SEARCH_LEVELS } from './config.js';
class DuckDuckGoServer {
private server: Server;
constructor() {
this.server = new Server(
{ name, version },
{
capabilities: {
tools: {},
},
},
);
this.setup_tool_handlers();
}
private setup_tool_handlers() {
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [
{
name: 'ddg_search',
description: 'Search the web using DuckDuckGo API',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
region: {
type: 'string',
description: 'Region code (e.g., us-en, uk-en)',
default: 'us-en',
},
safe_search: {
type: 'string',
description: 'Safe search level (off, moderate, strict)',
enum: ['off', 'moderate', 'strict'],
default: 'moderate',
},
date_filter: {
type: 'string',
description: 'Filter results by date (d: day, w: week, m: month, y: year, or custom range like 2023-01-01..2023-12-31)',
pattern: '^([dwmy]|\\d{4}-\\d{2}-\\d{2}\\.\\.\\d{4}-\\d{2}-\\d{2})$',
},
start: {
type: 'number',
description: 'Result offset for pagination',
minimum: 0,
},
no_cache: {
type: 'boolean',
description: 'Bypass cache and fetch fresh results',
default: false,
},
},
required: ['query'],
},
},
],
}),
);
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
if (request.params.name !== 'ddg_search') {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`,
);
}
const {
query,
region = 'us-en',
safe_search = 'moderate',
date_filter,
start,
no_cache = false,
} = request.params.arguments as {
query: string;
region?: string;
safe_search?: 'off' | 'moderate' | 'strict';
date_filter?: string;
start?: number;
no_cache?: boolean;
};
try {
// Check cache first if not explicitly bypassed
if (!no_cache) {
const cache_key = JSON.stringify({
query,
region,
safe_search,
date_filter,
start,
});
const cached_result = search_cache.get(cache_key);
if (cached_result) {
const formatted = format_response(cached_result);
return {
...formatted,
_meta: {},
} as ServerResult;
}
}
const params: Record<string, string> = {
engine: 'duckduckgo',
q: query,
kl: region,
safe: SAFE_SEARCH_LEVELS[safe_search],
api_key: API_KEY,
};
// Add date filter if provided
if (date_filter) {
params.df = date_filter;
}
// Add pagination if provided
if (start !== undefined) {
params.start = start.toString();
}
const search_params = new URLSearchParams(params);
const controller = new AbortController();
const timeout_id = setTimeout(
() => controller.abort(),
30000,
); // 30 second timeout
try {
const api_response = await fetch(
`${SERPAPI_BASE_URL}?${search_params.toString()}`,
{
method: 'GET',
signal: controller.signal,
},
);
if (!api_response.ok) {
throw new McpError(
ErrorCode.InternalError,
`Search API error: ${api_response.statusText}`,
);
}
const data: SerpApiResponse = await api_response.json();
// Cache the response if caching wasn't explicitly disabled
if (!no_cache) {
const cache_key = JSON.stringify({
query,
region,
safe_search,
date_filter,
start,
});
search_cache.set(cache_key, data);
}
const formatted = format_response(data);
return {
...formatted,
_meta: {},
} as ServerResult;
} finally {
clearTimeout(timeout_id);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error performing search: ${
error instanceof Error
? error.message
: String(error)
}`,
},
],
isError: true,
};
}
},
);
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('DuckDuckGo MCP server running on stdio');
}
}
const server = new DuckDuckGoServer();
server.run().catch(console.error);