Skip to main content
Glama

OneSearch MCP Server

index.ts10.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ISearchRequestOptions, ISearchResponse, SearchProvider } from './interface.js'; import { bingSearch, duckDuckGoSearch, searxngSearch, tavilySearch, localSearch } from './search/index.js'; import { SEARCH_TOOL, EXTRACT_TOOL, SCRAPE_TOOL, MAP_TOOL } from './tools.js'; import FirecrawlApp, { MapParams, ScrapeParams } from '@mendable/firecrawl-js'; import dotenvx from '@dotenvx/dotenvx'; import { SafeSearchType } from 'duck-duck-scrape'; dotenvx.config(); // search api const SEARCH_API_URL = process.env.SEARCH_API_URL; const SEARCH_API_KEY = process.env.SEARCH_API_KEY; const SEARCH_PROVIDER: SearchProvider = process.env.SEARCH_PROVIDER as SearchProvider ?? 'local'; // search query params const SAFE_SEARCH = process.env.SAFE_SEARCH ?? 0; const LIMIT = process.env.LIMIT ?? 10; const CATEGORIES = process.env.CATEGORIES ?? 'general'; const ENGINES = process.env.ENGINES ?? 'all'; const FORMAT = process.env.FORMAT ?? 'json'; const LANGUAGE = process.env.LANGUAGE ?? 'auto'; const TIME_RANGE = process.env.TIME_RANGE ?? ''; const DEFAULT_TIMEOUT = process.env.TIMEOUT ?? 10000; // firecrawl api const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY; const FIRECRAWL_API_URL = process.env.FIRECRAWL_API_URL; // firecrawl client const firecrawl = new FirecrawlApp({ apiKey: FIRECRAWL_API_KEY ?? '', ...(FIRECRAWL_API_URL ? { apiUrl: FIRECRAWL_API_URL } : {}), }); // Server implementation const server = new Server( { name: 'one-search-mcp', version: '0.0.1', }, { capabilities: { tools: {}, logging: {}, }, }, ); const searchDefaultConfig = { limit: Number(LIMIT), categories: CATEGORIES, format: FORMAT, safesearch: SAFE_SEARCH, language: LANGUAGE, engines: ENGINES, time_range: TIME_RANGE, timeout: DEFAULT_TIMEOUT, }; // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ SEARCH_TOOL, EXTRACT_TOOL, SCRAPE_TOOL, MAP_TOOL, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const startTime = Date.now(); try { const { name, arguments: args } = request.params; if (!args) { throw new Error('No arguments provided'); } server.sendLoggingMessage({ level: 'info', data: `[${new Date().toISOString()}] Received request for tool: [${name}]`, }); switch (name) { case 'one_search': { // check args. if (!checkSearchArgs(args)) { throw new Error(`Invalid arguments for tool: [${name}]`); } try { const { results, success } = await processSearch({ ...args, apiKey: SEARCH_API_KEY ?? '', apiUrl: SEARCH_API_URL, }); if (!success) { throw new Error('Failed to search'); } const resultsText = results.map((result) => ( `Title: ${result.title} URL: ${result.url} Description: ${result.snippet} ${result.markdown ? `Content: ${result.markdown}` : ''}` )); return { content: [ { type: 'text', text: resultsText.join('\n\n'), }, ], results, success, }; } catch (error) { server.sendLoggingMessage({ level: 'error', data: `[${new Date().toISOString()}] Error searching: ${error}`, }); const msg = error instanceof Error ? error.message : 'Unknown error'; return { success: false, content: [ { type: 'text', text: msg, }, ], }; } } case 'one_scrape': { if (!checkScrapeArgs(args)) { throw new Error(`Invalid arguments for tool: [${name}]`); } try { const startTime = Date.now(); server.sendLoggingMessage({ level: 'info', data: `[${new Date().toISOString()}] Scraping started for url: [${args.url}]`, }); const { url, ...scrapeArgs } = args; const { content, success, result } = await processScrape(url, scrapeArgs); server.sendLoggingMessage({ level: 'info', data: `[${new Date().toISOString()}] Scraping completed in ${Date.now() - startTime}ms`, }); return { content, result, success, }; } catch (error) { server.sendLoggingMessage({ level: 'error', data: `[${new Date().toISOString()}] Error scraping: ${error}`, }); const msg = error instanceof Error ? error.message : 'Unknown error'; return { success: false, content: [ { type: 'text', text: msg, }, ], }; } } case 'one_map': { if (!checkMapArgs(args)) { throw new Error(`Invalid arguments for tool: [${name}]`); } try { const { content, success, result } = await processMapUrl(args.url, args); return { content, result, success, }; } catch (error) { server.sendLoggingMessage({ level: 'error', data: `[${new Date().toISOString()}] Error mapping: ${error}`, }); const msg = error instanceof Error ? error.message : String(error); return { success: false, content: [ { type: 'text', text: msg, }, ], }; } } default: { throw new Error(`Unknown tool: ${name}`); } } } catch(error) { const msg = error instanceof Error ? error.message : String(error); server.sendLoggingMessage({ level: 'error', data: { message: `[${new Date().toISOString()}] Error processing request: ${msg}`, tool: request.params.name, arguments: request.params.arguments, timestamp: new Date().toISOString(), duration: Date.now() - startTime, }, }); return { success: false, content: [ { type: 'text', text: msg, }, ], }; } finally { server.sendLoggingMessage({ level: 'info', data: `[${new Date().toISOString()}] Request completed in ${Date.now() - startTime}ms`, }); } }); async function processSearch(args: ISearchRequestOptions): Promise<ISearchResponse> { switch (SEARCH_PROVIDER) { case 'searxng': { // merge default config with args const params = { ...searchDefaultConfig, ...args, apiKey: SEARCH_API_KEY, }; // but categories and language have higher priority (ENV > args). const { categories, language } = searchDefaultConfig; if (categories) { params.categories = categories; } if (language) { params.language = language; } return await searxngSearch(params); } case 'tavily': { return await tavilySearch({ ...searchDefaultConfig, ...args, apiKey: SEARCH_API_KEY, }); } case 'bing': { return await bingSearch({ ...searchDefaultConfig, ...args, apiKey: SEARCH_API_KEY, }); } case 'duckduckgo': { const safeSearch = args.safeSearch ?? 0; const safeSearchOptions = [SafeSearchType.STRICT, SafeSearchType.MODERATE, SafeSearchType.OFF]; return await duckDuckGoSearch({ ...searchDefaultConfig, ...args, apiKey: SEARCH_API_KEY, safeSearch: safeSearchOptions[safeSearch], }); } case 'local': { return await localSearch({ ...searchDefaultConfig, ...args, }); } default: throw new Error(`Unsupported search provider: ${SEARCH_PROVIDER}`); } } async function processScrape(url: string, args: ScrapeParams) { const res = await firecrawl.scrapeUrl(url, { ...args, }); if (!res.success) { throw new Error(`Failed to scrape: ${res.error}`); } const content: string[] = []; if (res.markdown) { content.push(res.markdown); } if (res.rawHtml) { content.push(res.rawHtml); } if (res.links) { content.push(res.links.join('\n')); } if (res.screenshot) { content.push(res.screenshot); } if (res.html) { content.push(res.html); } if (res.extract) { content.push(res.extract); } return { content: [ { type: 'text', text: content.join('\n\n') || 'No content found', }, ], result: res, success: true, }; } async function processMapUrl(url: string, args: MapParams) { const res = await firecrawl.mapUrl(url, { ...args, }); if ('error' in res) { throw new Error(`Failed to map: ${res.error}`); } if (!res.links) { throw new Error(`No links found from: ${url}`); } return { content: [ { type: 'text', text: res.links.join('\n').trim(), }, ], result: res.links, success: true, }; } function checkSearchArgs(args: unknown): args is ISearchRequestOptions { return ( typeof args === 'object' && args !== null && 'query' in args && typeof args.query === 'string' ); } function checkScrapeArgs(args: unknown): args is ScrapeParams & { url: string } { return ( typeof args === 'object' && args !== null && 'url' in args && typeof args.url === 'string' ); } function checkMapArgs(args: unknown): args is MapParams & { url: string } { return ( typeof args === 'object' && args !== null && 'url' in args && typeof args.url === 'string' ); } async function runServer() { try { process.stdout.write('Starting OneSearch MCP server...\n'); const transport = new StdioServerTransport(); await server.connect(transport); server.sendLoggingMessage({ level: 'info', data: 'OneSearch MCP server started', }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); process.stderr.write(`Error starting server: ${msg}\n`); process.exit(1); } } // run server runServer().catch((error) => { const msg = error instanceof Error ? error.message : String(error); process.stderr.write(`Error running server: ${msg}\n`); process.exit(1); }); // export types export * from './interface.js';

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yokingma/one-search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server