one-search-mcp
Version:
One Search MCP Server, Web Search & Crawl & Scraper & Extract, support Firecrawl, SearXNG, Tavily, DuckDuckGo, Bing, etc.
1 lines • 155 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/search/bing.ts","../src/search/duckduckgo.ts","../src/search/searxng.ts","../src/search/tavily.ts","../src/libs/browser/types.ts","../src/libs/browser/finder.ts","../src/libs/browser/base.ts","../src/libs/browser/local.ts","../src/libs/browser/remote.ts","../src/libs/browser-search/readability.ts","../src/libs/browser-search/search.ts","../src/libs/browser-search/utils.ts","../src/libs/browser-search/queue.ts","../src/libs/browser-search/engines/bing.ts","../src/libs/browser-search/engines/baidu.ts","../src/libs/browser-search/engines/sogou.ts","../src/libs/browser-search/engines/google.ts","../src/libs/browser-search/engines/get.ts","../src/search/local.ts","../src/tools.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { ISearchRequestOptions, ISearchResponse, SearchProvider } from './interface.js';\nimport { bingSearch, duckDuckGoSearch, searxngSearch, tavilySearch, localSearch } from './search/index.js';\nimport { SEARCH_TOOL, EXTRACT_TOOL, SCRAPE_TOOL, MAP_TOOL } from './tools.js';\nimport FirecrawlApp, { MapParams, ScrapeParams } from '@mendable/firecrawl-js';\nimport dotenvx from '@dotenvx/dotenvx';\nimport { SafeSearchType } from 'duck-duck-scrape';\n\ndotenvx.config();\n\n// search api\nconst SEARCH_API_URL = process.env.SEARCH_API_URL;\nconst SEARCH_API_KEY = process.env.SEARCH_API_KEY;\nconst SEARCH_PROVIDER: SearchProvider = process.env.SEARCH_PROVIDER as SearchProvider ?? 'local';\n\n// search query params\nconst SAFE_SEARCH = process.env.SAFE_SEARCH ?? 0;\nconst LIMIT = process.env.LIMIT ?? 10;\nconst CATEGORIES = process.env.CATEGORIES ?? 'general';\nconst ENGINES = process.env.ENGINES ?? 'all';\nconst FORMAT = process.env.FORMAT ?? 'json';\nconst LANGUAGE = process.env.LANGUAGE ?? 'auto';\nconst TIME_RANGE = process.env.TIME_RANGE ?? '';\nconst DEFAULT_TIMEOUT = process.env.TIMEOUT ?? 10000;\n\n// firecrawl api\nconst FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;\nconst FIRECRAWL_API_URL = process.env.FIRECRAWL_API_URL;\n\n// firecrawl client\nconst firecrawl = new FirecrawlApp({\n apiKey: FIRECRAWL_API_KEY ?? '',\n ...(FIRECRAWL_API_URL ? { apiUrl: FIRECRAWL_API_URL } : {}),\n});\n\n// Server implementation\nconst server = new Server(\n {\n name: 'one-search-mcp',\n version: '0.0.1',\n },\n {\n capabilities: {\n tools: {},\n logging: {},\n },\n },\n);\n\nconst searchDefaultConfig = {\n limit: Number(LIMIT),\n categories: CATEGORIES,\n format: FORMAT,\n safesearch: SAFE_SEARCH,\n language: LANGUAGE,\n engines: ENGINES,\n time_range: TIME_RANGE,\n timeout: DEFAULT_TIMEOUT,\n};\n\n// Tool handlers\nserver.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n SEARCH_TOOL,\n EXTRACT_TOOL,\n SCRAPE_TOOL,\n MAP_TOOL,\n ],\n}));\n\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n const startTime = Date.now();\n\n try {\n const { name, arguments: args } = request.params;\n\n if (!args) {\n throw new Error('No arguments provided');\n }\n \n server.sendLoggingMessage({\n level: 'info',\n data: `[${new Date().toISOString()}] Received request for tool: [${name}]`,\n });\n \n switch (name) {\n case 'one_search': {\n // check args.\n if (!checkSearchArgs(args)) {\n throw new Error(`Invalid arguments for tool: [${name}]`);\n }\n try {\n const { results, success } = await processSearch({\n ...args,\n apiKey: SEARCH_API_KEY ?? '',\n apiUrl: SEARCH_API_URL,\n });\n if (!success) {\n throw new Error('Failed to search');\n }\n const resultsText = results.map((result) => (\n `Title: ${result.title}\nURL: ${result.url}\nDescription: ${result.snippet}\n${result.markdown ? `Content: ${result.markdown}` : ''}`\n ));\n return {\n content: [\n {\n type: 'text',\n text: resultsText.join('\\n\\n'),\n },\n ],\n results,\n success,\n };\n } catch (error) {\n server.sendLoggingMessage({\n level: 'error',\n data: `[${new Date().toISOString()}] Error searching: ${error}`,\n });\n const msg = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n content: [\n {\n type: 'text',\n text: msg,\n },\n ],\n };\n }\n }\n case 'one_scrape': {\n if (!checkScrapeArgs(args)) {\n throw new Error(`Invalid arguments for tool: [${name}]`);\n }\n try {\n const startTime = Date.now();\n server.sendLoggingMessage({\n level: 'info',\n data: `[${new Date().toISOString()}] Scraping started for url: [${args.url}]`,\n });\n\n const { url, ...scrapeArgs } = args;\n const { content, success, result } = await processScrape(url, scrapeArgs);\n\n server.sendLoggingMessage({\n level: 'info',\n data: `[${new Date().toISOString()}] Scraping completed in ${Date.now() - startTime}ms`,\n });\n\n return {\n content,\n result,\n success,\n };\n } catch (error) {\n server.sendLoggingMessage({\n level: 'error',\n data: `[${new Date().toISOString()}] Error scraping: ${error}`,\n });\n const msg = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n content: [\n {\n type: 'text',\n text: msg,\n },\n ],\n };\n }\n }\n case 'one_map': {\n if (!checkMapArgs(args)) {\n throw new Error(`Invalid arguments for tool: [${name}]`);\n }\n try {\n const { content, success, result } = await processMapUrl(args.url, args);\n return {\n content,\n result,\n success,\n };\n } catch (error) {\n server.sendLoggingMessage({\n level: 'error',\n data: `[${new Date().toISOString()}] Error mapping: ${error}`,\n });\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n content: [\n {\n type: 'text',\n text: msg,\n },\n ],\n };\n }\n }\n default: {\n throw new Error(`Unknown tool: ${name}`);\n }\n }\n } catch(error) {\n const msg = error instanceof Error ? error.message : String(error);\n server.sendLoggingMessage({\n level: 'error',\n data: {\n message: `[${new Date().toISOString()}] Error processing request: ${msg}`,\n tool: request.params.name,\n arguments: request.params.arguments,\n timestamp: new Date().toISOString(),\n duration: Date.now() - startTime,\n },\n });\n return {\n success: false,\n content: [\n {\n type: 'text',\n text: msg,\n },\n ],\n };\n } finally {\n server.sendLoggingMessage({\n level: 'info',\n data: `[${new Date().toISOString()}] Request completed in ${Date.now() - startTime}ms`,\n });\n }\n});\n\nasync function processSearch(args: ISearchRequestOptions): Promise<ISearchResponse> {\n switch (SEARCH_PROVIDER) {\n case 'searxng': {\n // merge default config with args\n const params = {\n ...searchDefaultConfig,\n ...args,\n apiKey: SEARCH_API_KEY,\n };\n\n // but categories and language have higher priority (ENV > args).\n const { categories, language } = searchDefaultConfig;\n\n if (categories) {\n params.categories = categories;\n }\n if (language) {\n params.language = language;\n }\n return await searxngSearch(params);\n }\n case 'tavily': {\n return await tavilySearch({\n ...searchDefaultConfig,\n ...args,\n apiKey: SEARCH_API_KEY,\n });\n }\n case 'bing': {\n return await bingSearch({\n ...searchDefaultConfig,\n ...args,\n apiKey: SEARCH_API_KEY,\n });\n }\n case 'duckduckgo': {\n const safeSearch = args.safeSearch ?? 0;\n const safeSearchOptions = [SafeSearchType.STRICT, SafeSearchType.MODERATE, SafeSearchType.OFF];\n return await duckDuckGoSearch({\n ...searchDefaultConfig,\n ...args,\n apiKey: SEARCH_API_KEY,\n safeSearch: safeSearchOptions[safeSearch],\n });\n }\n case 'local': {\n return await localSearch({\n ...searchDefaultConfig,\n ...args,\n });\n }\n default:\n throw new Error(`Unsupported search provider: ${SEARCH_PROVIDER}`);\n }\n}\n\nasync function processScrape(url: string, args: ScrapeParams) {\n const res = await firecrawl.scrapeUrl(url, {\n ...args,\n });\n\n if (!res.success) {\n throw new Error(`Failed to scrape: ${res.error}`);\n }\n\n const content: string[] = [];\n\n if (res.markdown) {\n content.push(res.markdown);\n }\n\n if (res.rawHtml) {\n content.push(res.rawHtml);\n }\n\n if (res.links) {\n content.push(res.links.join('\\n'));\n }\n\n if (res.screenshot) {\n content.push(res.screenshot);\n }\n\n if (res.html) {\n content.push(res.html);\n }\n\n if (res.extract) {\n content.push(res.extract);\n }\n\n return {\n content: [\n {\n type: 'text',\n text: content.join('\\n\\n') || 'No content found',\n },\n ],\n result: res,\n success: true,\n };\n}\n\nasync function processMapUrl(url: string, args: MapParams) {\n const res = await firecrawl.mapUrl(url, {\n ...args,\n });\n\n if ('error' in res) {\n throw new Error(`Failed to map: ${res.error}`);\n }\n\n if (!res.links) {\n throw new Error(`No links found from: ${url}`);\n }\n\n return {\n content: [\n {\n type: 'text',\n text: res.links.join('\\n').trim(),\n },\n ],\n result: res.links,\n success: true,\n };\n}\n\nfunction checkSearchArgs(args: unknown): args is ISearchRequestOptions {\n return (\n typeof args === 'object' &&\n args !== null &&\n 'query' in args &&\n typeof args.query === 'string'\n );\n}\n\nfunction checkScrapeArgs(args: unknown): args is ScrapeParams & { url: string } {\n return (\n typeof args === 'object' &&\n args !== null &&\n 'url' in args &&\n typeof args.url === 'string'\n );\n}\n\nfunction checkMapArgs(args: unknown): args is MapParams & { url: string } {\n return (\n typeof args === 'object' &&\n args !== null &&\n 'url' in args &&\n typeof args.url === 'string'\n );\n}\n\nasync function runServer() {\n try {\n process.stdout.write('Starting OneSearch MCP server...\\n');\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n server.sendLoggingMessage({\n level: 'info',\n data: 'OneSearch MCP server started',\n });\n\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n process.stderr.write(`Error starting server: ${msg}\\n`);\n process.exit(1);\n }\n}\n\n// run server\nrunServer().catch((error) => {\n const msg = error instanceof Error ? error.message : String(error);\n process.stderr.write(`Error running server: ${msg}\\n`);\n process.exit(1);\n});\n\n// export types\nexport * from './interface.js';\n","/**\n * Bing Search API\n */\nimport { ISearchRequestOptions, ISearchResponse } from '../interface.js';\n\n\n/**\n * Options for performing a Bing search\n */\nexport interface BingSearchOptions {\n /**\n * Search query string\n */\n q: string;\n\n /**\n * Number of results to return\n */\n count?: number;\n\n /**\n * Result offset for pagination\n */\n offset?: number;\n\n /**\n * Market code (e.g., 'en-US')\n */\n mkt?: string;\n\n /**\n * Safe search filtering level\n */\n safeSearch?: 'Off' | 'Moderate' | 'Strict';\n\n /**\n * Bing API key\n */\n apiKey: string;\n\n /**\n * Bing Search API URL\n */\n apiUrl?: string;\n\n /**\n * Additional parameters supported by Bing Search API\n */\n [key: string]: any;\n}\n\n/**\n * Represents a web page result from Bing Search\n */\nexport interface BingSearchWebPage {\n /**\n * Title of the web page\n */\n name: string;\n\n /**\n * URL of the web page\n */\n url: string;\n\n /**\n * Text snippet from the web page\n */\n snippet: string;\n\n /**\n * Date the page was last crawled by Bing\n */\n dateLastCrawled?: string;\n\n /**\n * Display URL for the web page\n */\n displayUrl?: string;\n\n /**\n * Unique identifier for the result\n */\n id?: string;\n\n /**\n * Indicates if the content is family friendly\n */\n isFamilyFriendly?: boolean;\n\n /**\n * Indicates if the result is navigational\n */\n isNavigational?: boolean;\n\n /**\n * Language of the web page\n */\n language?: string;\n\n /**\n * Indicates if caching should be disabled\n */\n noCache?: boolean;\n\n /**\n * Name of the website\n */\n siteName?: string;\n\n /**\n * URL to a thumbnail image\n */\n thumbnailUrl?: string;\n}\n\n/**\n * Represents an image result from Bing Search\n */\nexport interface BingSearchImage {\n contentSize: string;\n contentUrl: string;\n datePublished: string;\n encodingFormat: string;\n height: number;\n width: number;\n hostPageDisplayUrl: string;\n hostPageUrl: string;\n name: string;\n thumbnail: {\n height: number;\n width: number;\n };\n thumbnailUrl: string;\n webSearchUrl: string;\n}\n\n/**\n * Represents a video result from Bing Search\n */\nexport interface BingSearchVideo {\n allowHttpsEmbed: boolean;\n allowMobileEmbed: boolean;\n contentUrl: string;\n creator?: {\n name: string;\n };\n datePublished: string;\n description: string;\n duration: string;\n embedHtml: string;\n encodingFormat: string;\n height: number;\n width: number;\n hostPageDisplayUrl: string;\n hostPageUrl: string;\n name: string;\n publisher?: {\n name: string;\n }[];\n thumbnail: {\n height: number;\n width: number;\n };\n thumbnailUrl: string;\n viewCount?: number;\n webSearchUrl: string;\n}\n\nexport interface BingSearchResponse {\n _type?: string;\n queryContext?: {\n originalQuery: string;\n };\n webPages?: {\n value: BingSearchWebPage[];\n totalEstimatedMatches?: number;\n someResultsRemoved?: boolean;\n webSearchUrl?: string;\n };\n images?: {\n value: BingSearchImage[];\n isFamilyFriendly?: boolean;\n readLink?: string;\n webSearchUrl?: string;\n id?: string;\n };\n videos?: {\n value: BingSearchVideo[];\n isFamilyFriendly?: boolean;\n readLink?: string;\n webSearchUrl?: string;\n id?: string;\n scenario?: string;\n };\n rankingResponse?: {\n mainline?: {\n items: {\n answerType: string;\n resultIndex?: number;\n value: {\n id: string;\n };\n }[];\n };\n };\n [key: string]: any; // Allow other response fields\n}\n\nexport async function bingSearch(options: ISearchRequestOptions): Promise<ISearchResponse> {\n const { query, limit = 10, safeSearch = 0, page = 1, apiUrl = 'https://api.bing.microsoft.com/v7.0/search', apiKey, language } = options;\n\n const bingSafeSearchOptions = ['Off', 'Moderate', 'Strict'];\n\n if (!apiKey) {\n throw new Error('Bing API key is required');\n }\n\n const searchOptions = {\n q: query,\n count: limit,\n offset: (page - 1) * limit,\n mkt: language,\n safeSearch: bingSafeSearchOptions[safeSearch] as 'Off' | 'Moderate' | 'Strict',\n };\n\n try {\n const queryParams = new URLSearchParams();\n Object.entries(searchOptions).forEach(([key, value]) => {\n if (value !== undefined) {\n queryParams.set(key, value.toString());\n }\n });\n\n const res = await fetch(`${apiUrl}?${queryParams}`, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'Ocp-Apim-Subscription-Key': apiKey,\n },\n });\n\n if (!res.ok) {\n throw new Error(`Bing search error: ${res.status} ${res.statusText}`);\n }\n\n const data = await res.json();\n const serp = data.webPages?.value as Array<BingSearchWebPage>;\n const results = serp?.map((item: BingSearchWebPage) => ({\n title: item.name,\n snippet: item.snippet,\n url: item.url,\n source: item.siteName,\n thumbnailUrl: item.thumbnailUrl,\n language: item.language,\n image: null,\n video: null,\n engine: 'bing',\n })) ?? [];\n\n return {\n results,\n success: true,\n };\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : 'Bing search error.';\n process.stdout.write(msg);\n throw err;\n }\n}","import * as DDG from 'duck-duck-scrape';\nimport asyncRetry from 'async-retry';\nimport type { SearchOptions } from 'duck-duck-scrape';\nimport { ISearchRequestOptions, ISearchResponse } from '../interface.js';\n\n\nexport async function duckDuckGoSearch(options: Omit<ISearchRequestOptions, 'safeSearch'> & SearchOptions): Promise<ISearchResponse> {\n try {\n const { query, timeout = 10000, safeSearch = DDG.SafeSearchType.OFF, retry = { retries: 3 }, ...searchOptions } = options;\n \n const res = await asyncRetry(\n () => {\n return DDG.search(query, {\n ...searchOptions,\n safeSearch,\n }, {\n // needle options\n response_timeout: timeout,\n });\n },\n retry,\n );\n\n const results = res ? {\n noResults: res.noResults,\n vqd: res.vqd,\n results: res.results,\n } : {\n noResults: true,\n vqd: '',\n results: [],\n };\n\n return {\n results: results.results.map((result) => ({\n title: result.title,\n snippet: result.description,\n url: result.url,\n source: result.hostname,\n image: null,\n video: null,\n engine: 'duckduckgo',\n })),\n success: true,\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : 'DuckDuckGo search error.';\n process.stdout.write(msg);\n throw error;\n }\n}\n","import url from 'node:url';\nimport { ISearchRequestOptions, ISearchResponse, ISearchResponseResult } from '../interface.js';\n\n/**\n * SearxNG Search API\n * - https://docs.searxng.org/dev/search_api.html\n */\nexport async function searxngSearch(params: ISearchRequestOptions): Promise<ISearchResponse> {\n try {\n const {\n query,\n page = 1,\n limit = 10,\n categories = 'general',\n engines = 'all',\n safeSearch = 0,\n format = 'json',\n language = 'auto',\n timeRange = '',\n timeout = 10000,\n apiKey,\n apiUrl,\n } = params;\n\n if (!apiUrl) {\n throw new Error('SearxNG API URL is required');\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), Number(timeout));\n\n const config = {\n q: query,\n pageno: page,\n categories,\n format,\n safesearch: safeSearch,\n language,\n engines,\n time_range: timeRange,\n };\n\n const endpoint = `${apiUrl}/search`;\n\n const queryParams = url.format({ query: config });\n\n const headers: HeadersInit = {\n 'Content-Type': 'application/json',\n };\n\n if (apiKey) {\n headers['Authorization'] = `Bearer ${apiKey}`;\n }\n\n const res = await fetch(`${endpoint}${queryParams}`, {\n method: 'POST',\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n const response = await res.json();\n if (response.results) {\n const list = (response.results as Array<Record<string, any>>).slice(0, limit);\n const results: ISearchResponseResult[] = list.map((item: Record<string, any>) => {\n const image = item.img_src ? {\n thumbnail: item.thumbnail_src,\n src: item.img_src,\n } : null;\n const video = item.iframe_src ? {\n thumbnail: item.thumbnail_src,\n src: item.iframe_src,\n } : null;\n return {\n title: item.title,\n snippet: item.content,\n url: item.url,\n source: item.source,\n image,\n video,\n engine: item.engine,\n };\n });\n return {\n results,\n success: true,\n };\n }\n return {\n results: [],\n success: false,\n };\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : 'Searxng search error.';\n process.stdout.write(msg);\n throw err;\n }\n}\n","import { tavily, TavilySearchOptions } from '@tavily/core';\nimport { ISearchRequestOptions, ISearchResponse } from '../interface.js';\n\n/**\n * Tavily Search API\n * - https://docs.tavily.com/documentation/quickstart\n */\nexport async function tavilySearch(options: ISearchRequestOptions): Promise<ISearchResponse> {\n const {\n query,\n limit = 10,\n categories = 'general',\n timeRange,\n apiKey,\n } = options;\n\n if (!apiKey) {\n throw new Error('Tavily API key is required');\n }\n\n try {\n const tvly = tavily({\n apiKey,\n });\n \n const params: TavilySearchOptions = {\n topic: categories as TavilySearchOptions['topic'],\n timeRange: timeRange as TavilySearchOptions['timeRange'],\n maxResults: limit,\n };\n \n const res = await tvly.search(query, params);\n const results = res.results.map(item => ({\n title: item.title,\n url: item.url,\n snippet: item.content,\n engine: 'tavily',\n }));\n \n return {\n results,\n success: true,\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : 'Tavily search error.';\n process.stdout.write(msg);\n throw error;\n }\n}","/**\n * The following code is based on\n * https://github.com/bytedance/UI-TARS-desktop/tree/main/packages/agent-infra/browser\n * \n * Copyright (c) 2025 Bytedance, Inc. and its affiliates.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Page, WaitForOptions } from 'puppeteer-core';\n\n/**\n * Options for launching a browser instance\n * @interface LaunchOptions\n */\nexport interface LaunchOptions {\n /**\n * Whether to run browser in headless mode\n * @default false\n */\n headless?: boolean;\n\n /**\n * Maximum time in milliseconds to wait for the browser to start\n * @default 0 (no timeout)\n */\n timeout?: number;\n\n /**\n * The viewport dimensions\n * @property {number} width - Viewport width in pixels\n * @property {number} height - Viewport height in pixels\n */\n defaultViewport?: {\n width: number;\n height: number;\n };\n\n /**\n * Path to a browser executable to use instead of the automatically detected one\n * If not provided, the system will attempt to find an installed browser\n */\n executablePath?: string;\n\n /**\n * Path to a specific browser profile to use\n * Allows using existing browser profiles with cookies, extensions, etc.\n */\n profilePath?: string;\n\n /**\n * Proxy server URL, e.g. 'http://proxy.example.com:8080'\n * Used to route browser traffic through a proxy server\n */\n proxy?: string;\n}\n\n/**\n * Options for evaluating JavaScript in a new page\n * @template T - Array of parameters to pass to the page function\n * @template R - Return type of the page function\n * @interface EvaluateOnNewPageOptions\n */\nexport interface EvaluateOnNewPageOptions<T extends any[], R> {\n /**\n * URL to navigate to before evaluating the function\n * The page will load this URL before executing the pageFunction\n */\n url: string;\n\n /**\n * Options for waiting for the page to load\n */\n waitForOptions?: WaitForOptions;\n\n /**\n * Function to be evaluated in the page context\n * This function runs in the context of the browser page, not Node.js\n * @param {Window} window - The window object of the page\n * @param {...T} args - Additional arguments passed to the function\n * @returns {R} Result of the function execution\n */\n pageFunction: (window: Window, ...args: T) => R;\n\n /**\n * Parameters to pass to the page function\n * These values will be serialized and passed to the pageFunction\n */\n pageFunctionParams: T;\n\n /**\n * Optional function to execute before page navigation\n * Useful for setting up page configuration before loading the URL\n * @param {Page} page - Puppeteer page instance\n * @returns {void | Promise<void>}\n */\n beforePageLoad?: (page: Page) => void | Promise<void>;\n\n /**\n * Optional function to execute after page navigation\n * Useful for setting up page configuration after loading the URL\n * @param {Page} page - Puppeteer page instance\n * @returns {void | Promise<void>}\n */\n afterPageLoad?: (page: Page) => void | Promise<void>;\n\n /**\n * Optional function to process the result before returning\n * Can be used to transform or validate the result from page evaluation\n * @param {Page} page - Puppeteer page instance\n * @param {R} result - Result from page function evaluation\n * @returns {R | Promise<R>} Processed result\n */\n beforeSendResult?: (page: Page, result: R) => R | Promise<R>;\n}\n\n/**\n * Core browser interface that all browser implementations must implement\n * Defines the standard API for browser automation\n * @interface BrowserInterface\n */\nexport interface BrowserInterface {\n /**\n * Launch a new browser instance\n * @param {LaunchOptions} [options] - Launch configuration options\n * @returns {Promise<void>} Promise resolving when browser is launched\n */\n launch(options?: LaunchOptions): Promise<void>;\n\n /**\n * Close the browser instance and all its pages\n * @returns {Promise<void>} Promise resolving when browser is closed\n */\n close(): Promise<void>;\n\n /**\n * Create a new page in the browser\n * @returns {Promise<Page>} Promise resolving to the new page instance\n */\n createPage(): Promise<Page>;\n\n /**\n * Evaluate a function in a new page context\n * Creates a new page, navigates to URL, executes function, and returns result\n * @template T - Array of parameters to pass to the page function\n * @template R - Return type of the page function\n * @param {EvaluateOnNewPageOptions<T, R>} options - Evaluation options\n * @returns {Promise<R | null>} Promise resolving to the function result or null\n */\n evaluateOnNewPage<T extends any[], R>(\n options: EvaluateOnNewPageOptions<T, R>,\n ): Promise<R | null>;\n\n /**\n * Get the currently active page or create one if none exists\n * @returns {Promise<Page>} Promise resolving to the active page instance\n */\n getActivePage(): Promise<Page>;\n}\n\nexport { Page };","/**\n * The following code is modified based on\n * https://github.com/egoist/local-web-search/blob/main/src/find-browser.ts\n * Copy from\n * https://github.com/bytedance/UI-TARS-desktop/blob/main/packages/agent-infra/browser/src/browser-finder.ts\n * \n * MIT Licensed\n * Copyright (c) 2025 ChatWise (https://chatwise.app) <kevin@chatwise.app>\n * https://github.com/egoist/local-web-search/blob/main/LICENSE\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { Logger, defaultLogger } from '@agent-infra/logger';\n\n/**\n * Interface defining browser locations and configurations\n * Contains paths and settings for different operating systems\n * @interface Browser\n */\ninterface Browser {\n /**\n * Browser name identifier\n */\n name: string;\n\n /**\n * Executable paths by platform\n * @property {string} win32 - Windows executable path\n * @property {string} darwin - macOS executable path\n * @property {string} linux - Linux executable path\n */\n executable: {\n win32: string;\n darwin: string;\n linux: string;\n };\n\n /**\n * User data directory paths by platform\n * @property {string} win32 - Windows user data directory\n * @property {string} darwin - macOS user data directory\n * @property {string} linux - Linux user data directory\n */\n userDataDir: {\n win32: string;\n darwin: string;\n linux: string;\n };\n}\n\n/**\n * Class responsible for finding and managing browser installations\n * Detects installed browsers and their profiles across different platforms\n */\nexport class BrowserFinder {\n /**\n * Logger instance for diagnostic output\n */\n private logger: Logger;\n\n /**\n * Creates a new BrowserFinder instance\n * @param {Logger} [logger] - Optional custom logger\n */\n constructor(logger?: Logger) {\n this.logger = logger ?? defaultLogger;\n }\n\n /**\n * Getter that returns the list of supported browsers with their platform-specific paths\n * @returns {Browser[]} Array of browser configurations\n * @private\n */\n private get browsers(): Browser[] {\n // Get HOME_DIR inside the getter to ensure it's always current\n const HOME_DIR = os.homedir();\n const LOCAL_APP_DATA = process.env.LOCALAPPDATA;\n\n return [\n {\n name: 'Chromium',\n executable: {\n win32: 'C:\\\\Program Files\\\\Chromium\\\\Application\\\\chrome.exe',\n darwin: '/Applications/Chromium.app/Contents/MacOS/Chromium',\n linux: '/usr/bin/chromium',\n },\n userDataDir: {\n win32: `${LOCAL_APP_DATA}\\\\Chromium\\\\User Data`,\n darwin: `${HOME_DIR}/Library/Application Support/Chromium`,\n linux: `${HOME_DIR}/.config/chromium`,\n },\n },\n {\n name: 'Google Chrome',\n executable: {\n win32: 'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n darwin:\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n linux: '/usr/bin/google-chrome',\n },\n userDataDir: {\n win32: `${LOCAL_APP_DATA}\\\\Google\\\\Chrome\\\\User Data`,\n darwin: `${HOME_DIR}/Library/Application Support/Google/Chrome`,\n linux: `${HOME_DIR}/.config/google-chrome`,\n },\n },\n {\n name: 'Google Chrome Canary',\n executable: {\n win32:\n 'C:\\\\Program Files\\\\Google\\\\Chrome Canary\\\\Application\\\\chrome.exe',\n darwin:\n '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',\n linux: '/usr/bin/google-chrome-canary',\n },\n userDataDir: {\n win32: `${LOCAL_APP_DATA}\\\\Google\\\\Chrome Canary\\\\User Data`,\n darwin: `${HOME_DIR}/Library/Application Support/Google/Chrome Canary`,\n linux: `${HOME_DIR}/.config/google-chrome-canary`,\n },\n },\n ];\n }\n\n /**\n * Find a specific browser or the first available browser\n * @param {string} [name] - Optional browser name to find\n * @returns {{ executable: string; userDataDir: string }} Browser executable and user data paths\n * @throws {Error} If no supported browser is found or the platform is unsupported\n */\n findBrowser(name?: string): {\n executable: string;\n userDataDir: string;\n } {\n const platform = process.platform;\n this.logger.info('Finding browser on platform:', platform);\n\n if (platform !== 'darwin' && platform !== 'win32' && platform !== 'linux') {\n const error = new Error(`Unsupported platform: ${platform}`);\n this.logger.error(error.message);\n throw error;\n }\n\n const browser = name\n ? this.browsers.find(\n (b) => b.name === name && fs.existsSync(b.executable[platform]),\n )\n : this.browsers.find((b) => fs.existsSync(b.executable[platform]));\n\n this.logger.log('browser', browser);\n\n if (!browser) {\n const error = name\n ? new Error(`Cannot find browser: ${name}`)\n : new Error(\n 'Cannot find a supported browser on your system. Please install Chrome, Edge, or Brave.',\n );\n this.logger.error(error.message);\n throw error;\n }\n\n const result = {\n executable: browser.executable[platform],\n userDataDir: browser.userDataDir[platform],\n };\n\n this.logger.success(`Found browser: ${browser.name}`);\n this.logger.info('Browser details:', result);\n\n return result;\n }\n\n /**\n * Get browser profiles for a specific browser\n * Reads the Local State file to extract profile information\n * @param {string} [browserName] - Optional browser name to get profiles for\n * @returns {Array<{ displayName: string; path: string }>} Array of profile objects with display names and paths\n */\n getBrowserProfiles(\n browserName?: string,\n ): Array<{ displayName: string; path: string }> {\n const browser = this.findBrowser(browserName);\n\n try {\n const localState = JSON.parse(\n fs.readFileSync(path.join(browser.userDataDir, 'Local State'), 'utf8'),\n );\n const profileInfo = localState.profile.info_cache;\n\n return Object.entries(profileInfo).map(\n ([profileName, info]: [string, any]) => ({\n displayName: info.name,\n path: path.join(browser.userDataDir, profileName),\n }),\n );\n } catch (error) {\n return [];\n }\n }\n\n /**\n * Legacy method for backwards compatibility\n * Finds Chrome browser executable path\n * @deprecated Use findBrowser instead\n * @returns {string | null} Chrome executable path or null if not found\n */\n findChrome(): string | null {\n try {\n const { executable } = this.findBrowser('Google Chrome');\n return executable;\n } catch {\n return null;\n }\n }\n}\n","/**\n * The following code is based on\n * https://github.com/bytedance/UI-TARS-desktop/tree/main/packages/agent-infra/browser\n * \n * Copyright (c) 2025 Bytedance, Inc. and its affiliates.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport * as puppeteer from 'puppeteer-core';\nimport { Logger, defaultLogger } from '@agent-infra/logger';\nimport {\n BrowserInterface,\n EvaluateOnNewPageOptions,\n LaunchOptions,\n Page,\n} from './types.js';\n\n/**\n * Configuration options for the BaseBrowser class\n * @interface BaseBrowserOptions\n * @property {Logger} [logger] - Custom logger instance to use for browser logging\n */\nexport interface BaseBrowserOptions {\n logger?: Logger;\n}\n\n/**\n * Abstract base class that implements common browser automation functionality\n * Provides a foundation for specific browser implementations with shared capabilities\n * @abstract\n * @implements {BrowserInterface}\n */\nexport abstract class BaseBrowser implements BrowserInterface {\n /**\n * The underlying Puppeteer browser instance\n * @protected\n */\n protected browser: puppeteer.Browser | null = null;\n\n /**\n * Logger instance for browser-related logging\n * @protected\n */\n protected logger: Logger;\n\n /**\n * Reference to the currently active browser page\n * @protected\n */\n protected activePage: Page | null = null;\n\n /**\n * Creates an instance of BaseBrowser\n * @param {BaseBrowserOptions} [options] - Configuration options\n */\n constructor(options?: BaseBrowserOptions) {\n this.logger = options?.logger ?? defaultLogger;\n this.logger.info('Browser Options:', options);\n }\n\n /**\n * Get the underlying Puppeteer browser instance\n * @throws Error if browser is not launched\n\n * @returns {puppeteer.Browser} Puppeteer browser instance\n */\n getBrowser(): puppeteer.Browser {\n if (!this.browser) {\n throw new Error('Browser not launched');\n }\n return this.browser;\n }\n\n /**\n * Sets up listeners for browser page events\n * Tracks page creation and updates active page reference\n * @protected\n */\n protected async setupPageListener() {\n if (!this.browser) return;\n\n this.browser.on('targetcreated', async (target) => {\n const page = await target.page();\n if (page) {\n this.logger.info('New page created:', await page.url());\n this.activePage = page;\n\n page.once('close', () => {\n if (this.activePage === page) {\n this.activePage = null;\n }\n });\n\n page.once('error', () => {\n if (this.activePage === page) {\n this.activePage = null;\n }\n });\n }\n });\n }\n\n /**\n * Launches the browser with specified options\n * @abstract\n * @param {LaunchOptions} [options] - Browser launch configuration options\n * @returns {Promise<void>} Promise that resolves when browser is launched\n */\n abstract launch(options?: LaunchOptions): Promise<void>;\n\n /**\n * Closes the browser instance and cleans up resources\n * @returns {Promise<void>} Promise that resolves when browser is closed\n * @throws {Error} If browser fails to close properly\n */\n async close(): Promise<void> {\n this.logger.info('Closing browser');\n try {\n await this.browser?.close();\n this.browser = null;\n this.logger.success('Browser closed successfully');\n } catch (error) {\n this.logger.error('Failed to close browser:', error);\n throw error;\n }\n }\n\n /**\n * Creates a new page, navigates to the specified URL, executes a function in the page context, and returns the result\n * This method is inspired and modified from https://github.com/egoist/local-web-search/blob/04608ed09aa103e2fff6402c72ca12edfb692d19/src/browser.ts#L74\n * @template T - Type of parameters passed to the page function\n * @template R - Return type of the page function\n * @param {EvaluateOnNewPageOptions<T, R>} options - Configuration options for the page evaluation\n * @returns {Promise<R | null>} Promise resolving to the result of the page function or null\n * @throws {Error} If page creation or evaluation fails\n */\n async evaluateOnNewPage<T extends any[], R>(\n options: EvaluateOnNewPageOptions<T, R>,\n ): Promise<R | null> {\n const {\n url,\n pageFunction,\n pageFunctionParams,\n beforePageLoad,\n afterPageLoad,\n beforeSendResult,\n waitForOptions,\n } = options;\n const page = await this.browser!.newPage();\n try {\n await beforePageLoad?.(page);\n await page.goto(url, {\n waitUntil: 'networkidle2',\n ...waitForOptions,\n });\n await afterPageLoad?.(page);\n const _window = await page.evaluateHandle(() => window);\n const result = await page.evaluate(\n pageFunction,\n _window,\n ...pageFunctionParams,\n );\n await beforeSendResult?.(page, result);\n await _window.dispose();\n await page.close();\n return result;\n } catch (error) {\n await page.close();\n throw error;\n }\n }\n\n /**\n * Creates a new browser page\n * @returns {Promise<Page>} Promise resolving to the newly created page\n * @throws {Error} If browser is not launched or page creation fails\n */\n async createPage(): Promise<Page> {\n if (!this.browser) {\n this.logger.error('No active browser');\n throw new Error('Browser not launched');\n }\n const page = await this.browser.newPage();\n return page;\n }\n\n /**\n * Gets the currently active page or finds an active page if none is currently tracked\n * If no active pages exist, creates a new page\n * @returns {Promise<Page>} Promise resolving to the active page\n * @throws {Error} If browser is not launched or no active page can be found/created\n */\n async getActivePage(): Promise<Page> {\n if (!this.browser) {\n throw new Error('Browser not launched');\n }\n\n // If activePage exists and is still available, return directly\n if (this.activePage) {\n try {\n // Verify that the page is still available\n await this.activePage.evaluate(() => document.readyState);\n return this.activePage;\n } catch (e) {\n this.logger.warn('Active page no longer available:', e);\n this.activePage = null;\n }\n }\n\n // Get all pages and find the last active page\n const pages = await this.browser.pages();\n\n if (pages.length === 0) {\n this.activePage = await this.createPage();\n return this.activePage;\n }\n\n // Find the last responding page\n for (let i = pages.length - 1; i >= 0; i--) {\n const page = pages[i];\n try {\n await page.evaluate(() => document.readyState);\n this.activePage = page;\n return page;\n } catch (e) {\n continue;\n }\n }\n\n throw new Error('No active page found');\n }\n}","/*\n * Copyright (c) 2025 Bytedance, Inc. and its affiliates.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport * as puppeteer from 'puppeteer-core';\nimport { LaunchOptions } from './types.js';\nimport { BrowserFinder } from './finder.js';\nimport { BaseBrowser } from './base.js';\n\n/**\n * LocalBrowser class for controlling locally installed browsers\n * Extends the BaseBrowser with functionality specific to managing local browser instances\n * @extends BaseBrowser\n */\nexport class LocalBrowser extends BaseBrowser {\n /**\n * Browser finder instance to detect and locate installed browsers\n * @private\n */\n private browserFinder = new BrowserFinder();\n\n /**\n * Launches a local browser instance with specified options\n * Automatically detects installed browsers if no executable path is provided\n * @param {LaunchOptions} options - Configuration options for launching the browser\n * @returns {Promise<void>} Promise that resolves when the browser is successfully launched\n * @throws {Error} If the browser cannot be launched\n */\n async launch(options: LaunchOptions = {}): Promise<void> {\n this.logger.info('Launching browser with options:', options);\n\n const executablePath =\n options?.executablePath || this.browserFinder.findBrowser().executable;\n\n this.logger.info('Using executable path:', executablePath);\n\n const viewportWidth = options?.defaultViewport?.width ?? 1280;\n const viewportHeight = options?.defaultViewport?.height ?? 800;\n\n const puppeteerLaunchOptions: puppeteer.LaunchOptions = {\n executablePath,\n headless: options?.headless ?? false,\n defaultViewport: {\n width: viewportWidth,\n height: viewportHeight,\n },\n args: [\n '--no-sandbox',\n '--mute-audio',\n '--disable-gpu',\n '--disable-http2',\n '--disable-blink-features=AutomationControlled',\n '--disable-infobars',\n '--disable-background-timer-throttling',\n '--disable-popup-blocking',\n '--disable-backgrounding-occluded-windows',\n '--disable-renderer-backgrounding',\n '--disable-window-activation',\n '--disable-focus-on-load',\n '--no-default-browser-check', // disable default browser check\n '--disable-web-security', // disable CORS\n '--disable-features=IsolateOrigins,site-per-process',\n '--disable-site-isolation-trials',\n `--window-size=${viewportWidth},${viewportHeight + 90}`,\n options?.proxy ? `--proxy-server=${options.proxy}` : '',\n options?.profilePath\n ? `--profile-directory=${options.profilePath}`\n : '',\n ].filter(Boolean),\n ignoreDefaultArgs: ['--enable-automation'],\n timeout: options.timeout ?? 0,\n downloadBehavior: {\n policy: 'deny',\n },\n };\n\n this.logger.info('Launch options:', puppeteerLaunchOptions);\n\n try {\n this.browser = await puppeteer.launch(puppeteerLaunchOptions);\n await this.setupPageListener();\n this.logger.success('Browser launched successfully');\n } catch (error) {\n this.logger.error('Failed to launch browser:', error);\n throw error;\n }\n }\n}","/*\n * Copyright (c) 2025 Bytedance, Inc. and its affiliates.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport * as puppeteer from 'puppeteer-core';\nimport { BaseBrowser, BaseBrowserOptions } from './base.js';\nimport { LaunchOptions } from './types.js';\n\n/**\n * Configuration options for RemoteBrowser\n * @extends BaseBrowserOptions\n * @interface RemoteBrowserOptions\n * @property {string} [wsEndpoint] - WebSocket endpoint URL for direct connection\n * @property {string} [host] - Remote host address (default: 'localhost')\n * @property {number} [port] - Remote debugging port (default: 9222)\n */\nexport interface RemoteBrowserOptions extends BaseBrowserOptions {\n wsEndpoint?: string;\n host?: string;\n port?: number;\n}\n\n/**\n * RemoteBrowser class for connecting to remote browser instances\n *\n * Currently, this RemoteBrowser is not production ready,\n * mainly because it still relies on `puppeteer-core`,\n * which can only run on Node.js.\n *\n * At the same time, Chrome instances built with\n * `--remote-debugging-address` on Linux have security risks\n *\n * @see https://issues.chromium.org/issues/41487252\n * @see https://issues.chromium.org/issues/40261787\n * @see https://github.com/pyppeteer/pyppeteer/pull/379\n * @see https://stackoverflow.com/questions/72760355/chrome-remote-debugging-not-working-computer-to-computer\n *\n * @extends BaseBrowser\n */\nexport class RemoteBrowser extends BaseBrowser {\n /**\n * Creates a new RemoteBrowser instance\n * @param {RemoteBrowserOptions} [options] - Configuration options for remote browser connection\n */\n constructor(private options?: RemoteBrowserOptions) {\n super(options);\n }\n\n /**\n * Connects to a remote browser instance using WebSocket\n * If no WebSocket endpoint is provided, attempts to discover it using the DevTools Protocol\n * @param {LaunchOptions} [options] - Launch configuration options\n * @returns {Promise<void>} Promise that resolves when connected to the remote browser\n * @throws {Error} If connection to the remote browser fails\n */\n async launch(options?: LaunchOptions): Promise<void> {\n this.logger.info('Browser Launch options:', options);\n\n let browserWSEndpoint = this.options?.wsEndpoint;\n\n if (!browserWSEndpoint) {\n const host = this.options?.host || 'localhost';\n const port = this.options?.port || 9222;\n const response = await fetch(`http://${host}:${port}/json/version`);\n const { webSocketDebuggerUrl } = await response.json();\n browserWSEndpoint = webSocketDebuggerUrl;\n }\n\n this.logger.info('Using WebSocket endpoint:', browserWSEndpoint);\n\n const puppeteerConnectOptions: puppeteer.ConnectOptions = {\n browserWSEndpoint,\n defaultViewport: options?.defaultViewport ?? { width: 1280, height: 800 },\n };\n\n try {\n this.browser = await puppeteer.connect(puppeteerConnectOptions);\n await this.setupPageListener();\n this.logger.success('Connected to remote browser successfully');\n } catch (error) {\n this.logger.error('Failed to connect to remote browser:', error);\n throw error;\n }\n }\n}","/**\n * PLEASE DO NOT MODIFY IT as it is generated by the build script\n *\n * Build: scripts/build-readability.ts\n * Source: https://github.com/mozilla/readability/blob/main/Readability.js\n */\n\n/**\n * Copyright (c) 2010 Arc90 Inc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const READABILITY_SCRIPT =\n 'function q(t,e){if(e&&e.documentElement)t=e,e=arguments[2];else if(!t||!t.documentElement)throw new Error(\"First argument to Readability constructor should be a document object.\");if(e=e||{},this._doc=t,this._docJSDOMParser=this._doc.firstChild.__JSDOMParser__,this._articleTitle=null,this._articleByline=null,this._articleDir=null,this._articleSiteName=null,this._attempts=[],this._debug=!!e.debug,this._maxElemsToParse=e.maxElemsToParse||this.DEFAULT_MAX_ELEMS_TO_PARSE,this._nbTopCandidates=e.nbTopCandidates||this.DEFAULT_N_TOP_CANDIDATES,this._charThreshold=e.charThreshold||this.DEFAULT_CHAR_THRESHOLD,this._classesToPreserve=this.CLASSES_TO_PRESERVE.concat(e.classesToPreserve||[]),this._keepClasses=!!e.keepClasses,this._serializer=e.serializer||function(i){return i.innerHTML},this._disableJSONLD=!!e.disableJSONLD,this._allowedVideoRegex=e.allowedVideoRegex||this.REGEXPS.videos,this._flags=this.FLAG_STRIP_UNLIKELYS|this.FLAG_WEIGHT_CLASSES|this.FLAG_CLEAN_CONDITIONALLY,this._debug){let i=function(r){if(r.nodeType==r.TEXT_NODE)return`${r.nodeName} (\"${r.textContent}\")`;l