#
tokens: 17762/50000 19/19 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .gitignore
├── .npmignore
├── .smithery
│   └── index.cjs
├── bin
│   └── cli.js
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── index.js
│   ├── index.ts
│   ├── tools
│   │   ├── iaskTool.js
│   │   ├── monicaTool.js
│   │   └── searchTool.js
│   └── utils
│       ├── search_iask.js
│       ├── search_monica.js
│       ├── search.js
│       └── user_agents.js
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | node_modules
 2 | npm-debug.log
 3 | .dockerignore
 4 | .git
 5 | .gitignore
 6 | .smithery
 7 | dist
 8 | .build
 9 | .idea
10 | .vscode
11 | **/*.md
12 | **/*.log
13 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | .qodo
 2 | 
 3 | # Dependency directories
 4 | node_modules/
 5 | npm-debug.log
 6 | yarn-debug.log
 7 | yarn-error.log
 8 | 
 9 | # Environment variables
10 | .env
11 | .env.local
12 | .env.development.local
13 | .env.test.local
14 | .env.production.local
15 | 
16 | # Build directories
17 | dist/
18 | build/
19 | 
20 | # IDE and editor files
21 | .idea/
22 | .vscode/
23 | *.swp
24 | *.swo
25 | .DS_Store
26 | 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # Development files
 2 | .git/
 3 | .github/
 4 | .vscode/
 5 | .idea/
 6 | .DS_Store
 7 | 
 8 | # Test files
 9 | test/
10 | tests/
11 | __tests__/
12 | coverage/
13 | 
14 | # Configuration files
15 | .eslintrc*
16 | .prettierrc*
17 | .editorconfig
18 | tsconfig.json
19 | jest.config.js
20 | 
21 | # Logs
22 | logs/
23 | *.log
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | 
28 | # Misc
29 | .qodo
30 | .env
31 | .env.*
32 | node_modules/
33 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | <div align="center">
  2 |   <a href="https://www.npmjs.com/package/@oevortex/ddg_search">
  3 |     <img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
  4 |   </a>
  5 |   <a href="https://github.com/OEvortex/ddg_search/blob/main/LICENSE">
  6 |     <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
  7 |   </a>
  8 |   <a href="https://youtube.com/@OEvortex">
  9 |     <img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
 10 |   </a>
 11 |   <h1>DuckDuckGo, IAsk AI & Monica Search MCP <span style="font-size:2.2rem;">🔍🧠</span></h1>
 12 |   <p style="font-size:1.15rem; max-width:600px; margin:0 auto;">
 13 |     <strong>Lightning-fast, privacy-first Model Context Protocol (MCP) server for web search and AI-powered answers.<br>
 14 |     Powered by DuckDuckGo, IAsk AI and Monica.</strong>
 15 |   </p>
 16 |   <a href="https://glama.ai/mcp/servers/@OEvortex/ddg_search">
 17 |     <img width="380" height="200" src="https://glama.ai/mcp/servers/@OEvortex/ddg_search/badge" alt="DuckDuckGo Search MCP server" />
 18 |   </a>
 19 |   <br>
 20 |   <a href="https://youtube.com/@OEvortex"><strong>Subscribe for updates & tutorials</strong></a>
 21 | </div>
 22 | 
 23 | ---
 24 | 
 25 | > [!IMPORTANT]
 26 | > DuckDuckGo Search MCP supports the Model Context Protocol (MCP) standard, making it compatible with various AI assistants and tools.
 27 | 
 28 | ---
 29 | 
 30 | ## ✨ Features
 31 | 
 32 | <div style="display: flex; flex-wrap: wrap; gap: 1.5em; margin-bottom: 1.5em;">  <div><b>🌐 Web search</b> using DuckDuckGo HTML</div>
 33 |   <div><b>🧠 AI search</b> using IAsk AI & Monica</div>
 34 |   <div><b>⚡ Performance optimized</b> with caching</div>
 35 |   <div><b>🛡️ Security features</b> including rate limiting and rotating user agents</div>
 36 |   <div><b>🔌 MCP-compliant</b> server implementation</div>
 37 |   <div><b>🆓 No API keys required</b> - works out of the box</div>
 38 | </div>
 39 | 
 40 | 
 41 | > [!IMPORTANT]
 42 | > Unlike many search tools, this package performs actual web scraping rather than using limited APIs, giving you more comprehensive results.
 43 | 
 44 | ---
 45 | 
 46 | ## 🚀 Quick Start
 47 | 
 48 | <div style="background: #222; color: #fff; padding: 1.5em; border-radius: 8px; margin: 1.5em 0;">
 49 | <b>Run instantly with npx:</b>
 50 | 
 51 | ```bash
 52 | npx -y @oevortex/ddg_search@latest
 53 | ```
 54 | </div>
 55 | 
 56 | 
 57 | > [!TIP]
 58 | > This will download and run the latest version of the MCP server directly without installation – perfect for quick use with AI assistants.
 59 | 
 60 | ---
 61 | 
 62 | ## 🛠️ Installation Options
 63 | 
 64 | <details>
 65 | <summary><b>Global Installation (npm)</b></summary>
 66 | 
 67 | ```bash
 68 | npm install -g @oevortex/ddg_search
 69 | ```
 70 | 
 71 | Run globally:
 72 | 
 73 | ```bash
 74 | ddg-search-mcp
 75 | ```
 76 | 
 77 | </details>
 78 | 
 79 | <details>
 80 | <summary><b>Global Installation (Yarn)</b></summary>
 81 | 
 82 | ```bash
 83 | yarn global add @oevortex/ddg_search
 84 | ```
 85 | 
 86 | Run globally:
 87 | 
 88 | ```bash
 89 | ddg-search-mcp
 90 | ```
 91 | 
 92 | </details>
 93 | 
 94 | <details>
 95 | <summary><b>Global Installation (pnpm)</b></summary>
 96 | 
 97 | ```bash
 98 | pnpm add -g @oevortex/ddg_search
 99 | ```
100 | 
101 | Run globally:
102 | 
103 | ```bash
104 | ddg-search-mcp
105 | ```
106 | 
107 | </details>
108 | 
109 | <details>
110 | <summary><b>Local Installation (Development)</b></summary>
111 | 
112 | ```bash
113 | git clone https://github.com/OEvortex/ddg_search.git
114 | cd ddg_search
115 | npm install
116 | npm start
117 | ```
118 | 
119 | Or with Yarn:
120 | 
121 | ```bash
122 | yarn install
123 | yarn start
124 | ```
125 | 
126 | Or with pnpm:
127 | 
128 | ```bash
129 | pnpm install
130 | pnpm start
131 | ```
132 | 
133 | </details>
134 | 
135 | ---
136 | 
137 | ## 🧑‍💻 Command Line Options
138 | 
139 | ```bash
140 | npx -y @oevortex/ddg_search@latest --help
141 | ```
142 | 
143 | > [!TIP]
144 | > Use the <code>--version</code> flag to check which version you're running.
145 | 
146 | ---
147 | 
148 | ## 🤖 Using with MCP Clients
149 | 
150 | > [!IMPORTANT]
151 | > The most common way to use this tool is by integrating it with MCP-compatible AI assistants.
152 | 
153 | Add the server to your MCP client configuration:
154 | 
155 | ```json
156 | {
157 |   "mcpServers": {
158 |     "ddg-search": {
159 |       "command": "npx",
160 |       "args": ["-y", "@oevortex/ddg_search@latest"]
161 |     }
162 |   }
163 | }
164 | ```
165 | 
166 | Or if installed globally:
167 | 
168 | ```json
169 | {
170 |   "mcpServers": {
171 |     "ddg-search": {
172 |       "command": "ddg-search-mcp"
173 |     }
174 |   }
175 | }
176 | ```
177 | 
178 | > [!TIP]
179 | > After configuring, restart your MCP client to apply the changes.
180 | 
181 | ---
182 | 
183 | ## 🧰 Tools Overview
184 | 
185 | <div style="display: flex; flex-wrap: wrap; gap: 2.5em; margin: 1.5em 0;">
186 |   <div style="margin-bottom: 1.5em;">
187 |     <b>🔍 Web Search Tool</b><br/>
188 |     <code>web-search</code><br/>
189 |     <ul>
190 |       <li><b>query</b> (string, required): The search query</li>
191 |       <li><b>page</b> (integer, optional, default: 1): Page number</li>
192 |       <li><b>numResults</b> (integer, optional, default: 10): Number of results (1-20)</li>
193 |     </ul>
194 |     <i>Example: Search the web for "climate change solutions"</i>
195 |   </div>
196 |   <div style="margin-bottom: 1.5em;">
197 |     <b>🧠 IAsk AI Search Tool</b><br/>
198 |     <code>iask-search</code><br/>
199 |     <ul>
200 |       <li><b>query</b> (string, required): The search query or question</li>
201 |       <li><b>mode</b> (string, optional, default: "question"): Search mode - "question", "academic", "forums", "wiki", or "thinking"</li>
202 |       <li><b>detailLevel</b> (string, optional): Response detail level - "concise", "detailed", or "comprehensive"</li>
203 |     </ul>
204 |     <i>Example: Search IAsk AI for "Explain quantum computing in simple terms"</i>
205 |   </div>
206 |   <div style="margin-bottom: 1.5em;">
207 |     <b>🤖 Monica AI Search Tool</b><br/>
208 |     <code>monica-search</code><br/>
209 |     <ul>
210 |       <li><b>query</b> (string, required): The search query or question</li>
211 |     </ul>
212 |     <i>Example: Search Monica AI for "Latest advancements in AI"</i>
213 |   </div>
214 | </div>
215 | 
216 | ---
217 | 
218 | ## 📁 Project Structure
219 | 
220 | 
221 | ```text
222 | bin/              # Command-line interface
223 | src/
224 |   index.js        # Main entry point
225 |   tools/          # Tool definitions and handlers
226 |     searchTool.js
227 |     iaskTool.js
228 |     monicaTool.js
229 |   utils/
230 |     search.js     # Search and URL utilities
231 |     user_agents.js
232 |     search_monica.js
233 |     search_iask.js # IAsk AI search utilities
234 | package.json
235 | README.md
236 | ```
237 | 
238 | ---
239 | 
240 | ## 🤝 Contributing
241 | 
242 | 
243 | Contributions are welcome! Please open issues or submit pull requests.
244 | 
245 | > [!NOTE]
246 | > Please follow the existing code style and add tests for new features.
247 | 
248 | ---
249 | 
250 | ## 📺 YouTube Channel
251 | 
252 | 
253 | <div align="center">
254 |   <a href="https://youtube.com/@OEvortex"><img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" /></a>
255 |   <br/>
256 |   <a href="https://youtube.com/@OEvortex">youtube.com/@OEvortex</a>
257 | </div>
258 | 
259 | ---
260 | 
261 | ## 📄 License
262 | 
263 | 
264 | Apache License 2.0
265 | 
266 | > [!NOTE]
267 | > This project is licensed under the Apache License 2.0 – see the <a href="LICENSE">LICENSE</a> file for details.
268 | 
269 | ---
270 | 
271 | <div align="center">
272 |   <sub>Made with ❤️ by <a href="https://youtube.com/@OEvortex">@OEvortex</a></sub>
273 | </div>
274 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | runtime: typescript
 2 | 
 3 | build:
 4 |   external:
 5 |     - canvas
 6 |     - utf-8-validate
 7 |     - bufferutil
 8 |   esbuild:
 9 |     bundle: true
10 |     platform: node
11 |     format: cjs
12 |     target: node18
13 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ES2022",
 5 |     "moduleResolution": "node",
 6 |     "allowSyntheticDefaultImports": true,
 7 |     "esModuleInterop": true,
 8 |     "allowJs": true,
 9 |     "checkJs": false,
10 |     "outDir": "./dist",
11 |     "rootDir": "./src",
12 |     "strict": false,
13 |     "skipLibCheck": true,
14 |     "forceConsistentCasingInFileNames": true,
15 |     "declaration": true,
16 |     "declarationMap": true,
17 |     "sourceMap": true,
18 |     "types": ["node"]
19 |   },
20 |   "include": [
21 |     "src/**/*"
22 |   ],
23 |   "exclude": [
24 |     "node_modules",
25 |     "dist",
26 |     "bin"
27 |   ]
28 | }
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Build stage: install dependencies
 2 | FROM node:22-slim AS build
 3 | WORKDIR /app
 4 | 
 5 | # Copy package manifests and lockfile first for better caching
 6 | COPY package.json package-lock.json ./
 7 | 
 8 | # Install production dependencies (use npm ci when lockfile exists)
 9 | RUN if [ -f package-lock.json ]; then npm ci --production; else npm install --production; fi
10 | 
11 | # Copy application source
12 | COPY . .
13 | 
14 | # Final minimal runtime image
15 | FROM node:22-slim AS runtime
16 | WORKDIR /app
17 | 
18 | # Copy node_modules and built app from build stage
19 | COPY --from=build /app/node_modules ./node_modules
20 | COPY --from=build /app .
21 | 
22 | # Expose port in case the MCP server needs it
23 | EXPOSE 3000
24 | 
25 | # Default command: use the CLI entry which starts the MCP server
26 | CMD ["node", "bin/cli.js"]
27 | 
```

--------------------------------------------------------------------------------
/src/tools/monicaTool.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { searchMonica } from '../utils/search_monica.js';
 2 | 
 3 | /**
 4 |  * Monica AI search tool definition
 5 |  */
 6 | export const monicaToolDefinition = {
 7 |   name: 'monica-search',
 8 |   title: 'Monica AI Search',
 9 |   description: 'AI-powered search using Monica AI. Returns AI-generated responses based on web content.',
10 |   inputSchema: {
11 |     type: 'object',
12 |     properties: {
13 |       query: {
14 |         type: 'string',
15 |         description: 'The search query or question.'
16 |       }
17 |     },
18 |     required: ['query']
19 |   },
20 |   annotations: {
21 |     readOnlyHint: true,
22 |     openWorldHint: false
23 |   }
24 | };
25 | 
26 | /**
27 |  * Monica AI search tool handler
28 |  * @param {Object} params - The tool parameters
29 |  * @returns {Promise<Object>} - The tool result
30 |  */
31 | export async function monicaToolHandler(params) {
32 |   const { query } = params;
33 |   
34 |   console.log(`Searching Monica AI for: "${query}"`);
35 |   
36 |   try {
37 |     const result = await searchMonica(query);
38 |     return {
39 |       content: [
40 |         {
41 |           type: 'text',
42 |           text: result || 'No results found.'
43 |         }
44 |       ]
45 |     };
46 |   } catch (error) {
47 |     console.error(`Error in Monica search: ${error.message}`);
48 |     return {
49 |       isError: true,
50 |       content: [
51 |         {
52 |           type: 'text',
53 |           text: `Error searching Monica: ${error.message}`
54 |         }
55 |       ]
56 |     };
57 |   }
58 | }
59 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@oevortex/ddg_search",
 3 |   "version": "1.1.8",
 4 |   "description": "A Model Context Protocol server for web search using DuckDuckGo and IAsk AI",
 5 |   "main": "src/index.js",
 6 |   "module": "src/index.ts",
 7 |   "exports": {
 8 |     ".": {
 9 |       "import": "./src/index.js",
10 |       "default": "./src/index.js"
11 |     }
12 |   },
13 |   "bin": {
14 |     "ddg-search-mcp": "bin/cli.js",
15 |     "oevortex-ddg-search": "bin/cli.js"
16 |   },
17 |   "scripts": {
18 |     "test": "echo \"Error: no test specified\" && exit 1",
19 |     "start": "node bin/cli.js",
20 |     "prepublishOnly": "npm run lint",
21 |     "lint": "echo \"No linting configured\"",
22 |     "build": "npx @smithery/cli build",
23 |     "dev": "npx @smithery/cli dev"
24 |   },
25 |   "publishConfig": {
26 |     "access": "public"
27 |   },
28 |   "keywords": [
29 |     "mcp",
30 |     "model-context-protocol",
31 |     "duckduckgo",
32 |     "iask",
33 |     "search",
34 |     "web-search",
35 |     "ai-search",
36 |     "claude",
37 |     "ai",
38 |     "llm"
39 |   ],
40 |   "author": "OEvortex",
41 |   "license": "Apache-2.0",
42 |   "type": "module",
43 |   "dependencies": {
44 |     "@modelcontextprotocol/sdk": "^1.17.4",
45 |     "axios": "^1.8.4",
46 |     "axios-cookiejar-support": "^6.0.5",
47 |     "cheerio": "^1.0.0",
48 |     "smithery": "^0.5.2",
49 |     "tough-cookie": "^6.0.0",
50 |     "turndown": "^7.2.2",
51 |     "ws": "^8.18.3"
52 |   },
53 |   "devDependencies": {
54 |     "@types/node": "^24.3.0",
55 |     "tsx": "^4.20.4",
56 |     "typescript": "^5.9.2"
57 |   }
58 | }
```

--------------------------------------------------------------------------------
/src/tools/searchTool.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { searchDuckDuckGo } from '../utils/search.js';
 2 | 
 3 | /**
 4 |  * Web search tool definition
 5 |  */
 6 | export const searchToolDefinition = {
 7 |   name: 'web-search',
 8 |   title: 'Web Search',
 9 |   description: 'Perform a web search using DuckDuckGo and receive detailed results including titles, URLs, and summaries.',
10 |   inputSchema: {
11 |     type: 'object',
12 |     properties: {
13 |       query: {
14 |         type: 'string',
15 |         description: 'Enter your search query to find the most relevant web pages.'
16 |       },
17 |       numResults: {
18 |         type: 'integer',
19 |         description: 'Specify how many results to display (default: 3, maximum: 20).',
20 |         default: 3,
21 |         minimum: 1,
22 |         maximum: 20
23 |       },
24 |       mode: {
25 |         type: 'string',
26 |         description: "Choose 'short' for basic results (no Description) or 'detailed' for full results (includes Description).",
27 |         enum: ['short', 'detailed'],
28 |         default: 'short'
29 |       }
30 |     },
31 |     required: ['query']
32 |   }
33 | };
34 | 
35 | /**
36 |  * Web search tool handler
37 |  * @param {Object} params - The tool parameters
38 |  * @returns {Promise<Object>} - The tool result
39 |  */
40 | export async function searchToolHandler(params) {
41 |   const { query, numResults = 3, mode = 'short' } = params;
42 |   console.log(`Searching for: ${query} (${numResults} results, mode: ${mode})`);
43 | 
44 |   const results = await searchDuckDuckGo(query, numResults, mode);
45 |   console.log(`Found ${results.length} results`);
46 | 
47 |   return {
48 |     content: [
49 |       {
50 |         type: 'text',
51 |         text: JSON.stringify(results)
52 |       }
53 |     ]
54 |   };
55 | }
56 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | All notable changes to this project will be documented in this file.
 4 | ## [1.1.8] - 2025-12-03
 5 | ### Added
 6 | - Added new `getRandomUserAgent` function to rotate user agents
 7 | - Added new `src/utils/user_agents.js` file containing list of user agents
 8 | - switch to use `user_agents.js` file for user agent rotation
 9 | - Removed stream from iaskTool.js & search_iask.js
10 | - Added new `monica-search` tool for AI-powered search using Monica AI
11 | 
12 | ### Changed
13 | - Updated `src/index.ts` to use IAsk tool instead of Felo tool
14 | - Updated `package.json` description, keywords, and dependencies (`turndown`, `ws`)
15 | - Updated `README.md` to reference IAsk AI and document new tool parameters
16 | - Removed old Felo tool files (`feloTool.js`, `search_felo.js`)
17 | 
18 | ## [1.1.7] - 2025-11-30
19 | ### Changed
20 | - Replaced Felo AI tool with IAsk AI tool for advanced AI-powered search
21 | - Added new dependencies: `turndown` for HTML to Markdown conversion, `ws` for WebSocket support
22 | - Updated README to reflect changes and new tool usage
23 | - Added new modes: 'short', 'detailed' in web search tool
24 | - Added `src/utils/search_iask.js` implementing IAsk API client
25 | - Added `src/tools/iaskTool.js` tool definition and handler
26 | - Updated `src/index.ts` to use IAsk tool instead of Felo
27 | - Updated `package.json` description, keywords, and dependencies (`turndown`, `ws`)
28 | - Updated `README.md` to reference IAsk AI and document new tool parameters
29 | - Removed old Felo tool files (`feloTool.js`, `search_felo.js`)
30 | 
31 | ## [1.1.2] - 2025-11-29
32 | ### Added
33 | - Initial release with DuckDuckGo and Felo AI search tools
34 | - MCP server implementation
35 | - Caching, rotating user agents, and web scraping features
36 | 
```

--------------------------------------------------------------------------------
/src/utils/user_agents.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 |  * List of user agents for rotation
 3 |  */
 4 | const USER_AGENTS = [
 5 |   'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
 6 |   'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
 7 |   'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
 8 |   'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
 9 |   'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
10 |   'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
11 |   'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
12 |   'Mozilla/5.0 (iPad; CPU OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.119 Mobile/15E148 Safari/604.1',
13 |   'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
14 |   'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
15 |   'Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36',
16 |   'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/105.0.0.0',
17 |   'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Vivaldi/6.4.3160.42',
18 |   'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0',
19 | ];
20 | 
21 | /**
22 |  * Get a random user agent from the list
23 |  * @returns {string} A random user agent string
24 |  */
25 | export function getRandomUserAgent() {
26 |   return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
27 | }
28 | 
```

--------------------------------------------------------------------------------
/src/tools/iaskTool.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS } from '../utils/search_iask.js';
 2 | 
 3 | /**
 4 |  * IAsk AI search tool definition
 5 |  */
 6 | export const iaskToolDefinition = {
 7 |   name: 'iask-search',
 8 |   title: 'IAsk AI Search',
 9 |   description: 'AI-powered search using IAsk.ai. Retrieves comprehensive, AI-generated responses based on web content. Supports different search modes (question, academic, forums, wiki, thinking) and detail levels (concise, detailed, comprehensive). Ideal for getting well-researched answers to complex questions.',
10 |   inputSchema: {
11 |     type: 'object',
12 |     properties: {
13 |       query: {
14 |         type: 'string',
15 |         description: 'The search query or question to ask. Supports natural language questions for comprehensive AI-generated responses.'
16 |       },
17 |       mode: {
18 |         type: 'string',
19 |         description: 'Search mode to use. Options: "question" (general questions), "academic" (scholarly/research), "forums" (community discussions), "wiki" (encyclopedia-style), "thinking" (deep analysis). Default is "question".',
20 |         enum: VALID_MODES,
21 |         default: 'question'
22 |       },
23 |       detailLevel: {
24 |         type: 'string',
25 |         description: 'Level of detail in the response. Options: "concise" (brief), "detailed" (moderate), "comprehensive" (extensive). Default is null (standard response).',
26 |         enum: VALID_DETAIL_LEVELS
27 |       }
28 |     },
29 |     required: ['query']
30 |   },
31 |   annotations: {
32 |     readOnlyHint: true,
33 |     openWorldHint: false
34 |   }
35 | };
36 | 
37 | /**
38 |  * IAsk AI search tool handler
39 |  * @param {Object} params - The tool parameters
40 |  * @returns {Promise<Object>} - The tool result
41 |  */
42 | export async function iaskToolHandler(params) {
43 |   const { 
44 |     query, 
45 |     mode = 'thinking', 
46 |     detailLevel = null
47 |   } = params;
48 |   
49 |   console.log(`Searching IAsk AI for: "${query}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'})`);
50 |   
51 |   try {
52 |     const response = await searchIAsk(query, mode, detailLevel);
53 |     
54 |     return {
55 |       content: [
56 |         {
57 |           type: 'text',
58 |           text: response || 'No results found.'
59 |         }
60 |       ]
61 |     };
62 |   } catch (error) {
63 |     console.error(`Error in IAsk search: ${error.message}`);
64 |     return {
65 |       isError: true,
66 |       content: [
67 |         {
68 |           type: 'text',
69 |           text: `Error searching IAsk: ${error.message}`
70 |         }
71 |       ]
72 |     };
73 |   }
74 | }
75 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
 2 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
 3 | 
 4 | // Import tool definitions and handlers
 5 | import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
 6 | import { iaskToolDefinition, iaskToolHandler } from './tools/iaskTool.js';
 7 | import { monicaToolDefinition, monicaToolHandler } from './tools/monicaTool.js';
 8 | 
 9 | // Required: Export default createServer function for Smithery
10 | export default function createServer({ config }: { config?: any } = {}) {
11 |   console.log('Creating MCP server with latest SDK...');
12 | 
13 |   // Global variable to track available tools
14 |   const availableTools = [
15 |     searchToolDefinition,
16 |     iaskToolDefinition,
17 |     monicaToolDefinition
18 |   ];
19 |   
20 |   console.log('Available tools:', availableTools.map(t => t.name));
21 | 
22 |   // Create the MCP server using the Server class
23 |   const server = new Server({
24 |     name: 'ddg-search-mcp',
25 |     version: '1.1.2'
26 |   }, {
27 |     capabilities: {
28 |       tools: {
29 |         listChanged: true
30 |       }
31 |     }
32 |   });
33 | 
34 |   // Define available tools
35 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
36 |     console.log('Tools list requested, returning:', availableTools.length, 'tools');
37 |     return {
38 |       tools: availableTools
39 |     };
40 |   });
41 | 
42 |   // Handle tool execution
43 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
44 |     try {
45 |       const { name, arguments: args } = request.params;
46 |       console.log(`Tool call received: ${name} with args:`, args);
47 |       
48 |       // Route to the appropriate tool handler
49 |       switch (name) {
50 |         case 'web-search':
51 |           return await searchToolHandler(args);
52 | 
53 |         case 'iask-search':
54 |           return await iaskToolHandler(args);
55 | 
56 |         case 'monica-search':
57 |           return await monicaToolHandler(args);
58 | 
59 |         default:
60 |           throw new Error(`Tool not found: ${name}`);
61 |       }
62 |     } catch (error: any) {
63 |       console.error(`Error handling ${request.params.name} tool call:`, error);
64 |       
65 |       // Return proper tool execution error format
66 |       return {
67 |         isError: true,
68 |         content: [
69 |           {
70 |             type: 'text',
71 |             text: `Error executing tool '${request.params.name}': ${error.message}`
72 |           }
73 |         ]
74 |       };
75 |     }
76 |   });
77 | 
78 |   console.log('MCP server created successfully');
79 |   
80 |   // Return the server instance (required for Smithery)
81 |   return server;
82 | }
83 | 
84 | // Optional: No configuration schema needed for this server
85 | // export const configSchema = z.object({});
```

--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
  3 | 
  4 | // Import tool definitions and handlers
  5 | import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
  6 | import { iaskToolDefinition, iaskToolHandler } from './tools/iaskTool.js';
  7 | import { monicaToolDefinition, monicaToolHandler } from './tools/monicaTool.js';
  8 | 
  9 | // Required: Export default createServer function for Smithery
 10 | export default function createServer({ config } = {}) {
 11 |   console.log('Creating MCP server with latest SDK...');
 12 | 
 13 |   // Global variable to track available tools
 14 |   const availableTools = [
 15 |     searchToolDefinition,
 16 |     iaskToolDefinition,
 17 |     monicaToolDefinition
 18 |   ];
 19 |   
 20 |   console.log('Available tools:', availableTools.map(t => t.name));
 21 | 
 22 |   // Create the MCP server using the Server class
 23 |   const server = new Server({
 24 |     name: 'ddg-search-mcp',
 25 |     version: '1.1.2'
 26 |   }, {
 27 |     capabilities: {
 28 |       tools: {
 29 |         listChanged: true
 30 |       }
 31 |     }
 32 |   });
 33 | 
 34 |   // Define available tools
 35 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
 36 |     console.log('Tools list requested, returning:', availableTools.length, 'tools');
 37 |     return {
 38 |       tools: availableTools
 39 |     };
 40 |   });
 41 | 
 42 |   // Handle tool execution
 43 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
 44 |     try {
 45 |       const { name, arguments: args } = request.params;
 46 |       console.log(`Tool call received: ${name} with args:`, args);
 47 |       
 48 |       // Route to the appropriate tool handler
 49 |       switch (name) {
 50 |         case 'web-search':
 51 |           return await searchToolHandler(args);
 52 | 
 53 |         case 'iask-search':
 54 |           return await iaskToolHandler(args);
 55 | 
 56 |         case 'monica-search':
 57 |           return await monicaToolHandler(args);
 58 | 
 59 |         default:
 60 |           throw new Error(`Tool not found: ${name}`);
 61 |       }
 62 |     } catch (error) {
 63 |       console.error(`Error handling ${request.params.name} tool call:`, error);
 64 |       
 65 |       // Return proper tool execution error format
 66 |       return {
 67 |         isError: true,
 68 |         content: [
 69 |           {
 70 |             type: 'text',
 71 |             text: `Error executing tool '${request.params.name}': ${error.message}`
 72 |           }
 73 |         ]
 74 |       };
 75 |     }
 76 |   });
 77 | 
 78 |   console.log('MCP server created successfully');
 79 |   
 80 |   // Return the server instance (required for Smithery)
 81 |   return server;
 82 | }
 83 | 
 84 | // Legacy standalone server support (for CLI usage)
 85 | if (import.meta.url === `file://${process.argv[1]}`) {
 86 |   async function main() {
 87 |     try {
 88 |       const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
 89 |       const server = createServer();
 90 |       const transport = new StdioServerTransport();
 91 |       await server.connect(transport);
 92 |       console.error('WebSearch MCP server started and listening on stdio');
 93 |     } catch (error) {
 94 |       console.error('Failed to start server:', error);
 95 |       process.exit(1);
 96 |     }
 97 |   }
 98 | 
 99 |   main();
100 | }
101 | 
```

--------------------------------------------------------------------------------
/src/utils/search_monica.js:
--------------------------------------------------------------------------------

```javascript
  1 | import axios from 'axios';
  2 | import { randomUUID } from 'crypto';
  3 | import { getRandomUserAgent } from './user_agents.js';
  4 | 
  5 | class MonicaClient {
  6 |   constructor(timeout = 60000) {
  7 |     this.apiEndpoint = "https://monica.so/api/search_v1/search";
  8 |     this.timeout = timeout;
  9 |     this.clientId = randomUUID();
 10 |     this.sessionId = "";
 11 |     
 12 |     this.headers = {
 13 |       "accept": "*/*",
 14 |       "accept-encoding": "gzip, deflate, br, zstd",
 15 |       "accept-language": "en-US,en;q=0.9",
 16 |       "content-type": "application/json",
 17 |       "dnt": "1",
 18 |       "origin": "https://monica.so",
 19 |       "referer": "https://monica.so/answers",
 20 |       "sec-ch-ua": '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
 21 |       "sec-ch-ua-mobile": "?0",
 22 |       "sec-ch-ua-platform": '"Windows"',
 23 |       "sec-fetch-dest": "empty",
 24 |       "sec-fetch-mode": "cors",
 25 |       "sec-fetch-site": "same-origin",
 26 |       "sec-gpc": "1",
 27 |       "user-agent": getRandomUserAgent(),
 28 |       "x-client-id": this.clientId,
 29 |       "x-client-locale": "en",
 30 |       "x-client-type": "web",
 31 |       "x-client-version": "5.4.3",
 32 |       "x-from-channel": "NA",
 33 |       "x-product-name": "Monica-Search",
 34 |       "x-time-zone": "Asia/Calcutta;-330"
 35 |     };
 36 | 
 37 |     // Axios instance
 38 |     this.client = axios.create({
 39 |       headers: this.headers,
 40 |       timeout: this.timeout,
 41 |       withCredentials: true
 42 |     });
 43 |   }
 44 | 
 45 |   formatResponse(text) {
 46 |     // Clean up markdown formatting
 47 |     let cleanedText = text.replace(/\*\*/g, '');
 48 |     
 49 |     // Remove any empty lines
 50 |     cleanedText = cleanedText.replace(/\n\s*\n/g, '\n\n');
 51 |     
 52 |     // Remove any trailing whitespace
 53 |     return cleanedText.trim();
 54 |   }
 55 | 
 56 |   async search(prompt) {
 57 |     const taskId = randomUUID();
 58 |     const payload = {
 59 |       "pro": false,
 60 |       "query": prompt,
 61 |       "round": 1,
 62 |       "session_id": this.sessionId,
 63 |       "language": "auto",
 64 |       "task_id": taskId
 65 |     };
 66 | 
 67 |     const cookies = {
 68 |       "monica_home_theme": "auto"
 69 |     };
 70 |     
 71 |     // Convert cookies object to string
 72 |     const cookieString = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
 73 | 
 74 |     try {
 75 |       const response = await this.client.post(this.apiEndpoint, payload, {
 76 |         headers: {
 77 |           ...this.headers,
 78 |           'Cookie': cookieString
 79 |         },
 80 |         responseType: 'stream'
 81 |       });
 82 | 
 83 |       let fullText = '';
 84 |       
 85 |       return new Promise((resolve, reject) => {
 86 |         response.data.on('data', (chunk) => {
 87 |           const lines = chunk.toString().split('\n');
 88 |           for (const line of lines) {
 89 |             if (line.startsWith('data: ')) {
 90 |               try {
 91 |                 const jsonStr = line.substring(6);
 92 |                 const data = JSON.parse(jsonStr);
 93 | 
 94 |                 if (data.session_id) {
 95 |                   this.sessionId = data.session_id;
 96 |                 }
 97 | 
 98 |                 if (data.text) {
 99 |                   fullText += data.text;
100 |                 }
101 |               } catch (e) {
102 |                 // Ignore parse errors
103 |               }
104 |             }
105 |           }
106 |         });
107 | 
108 |         response.data.on('end', () => {
109 |           resolve(this.formatResponse(fullText));
110 |         });
111 | 
112 |         response.data.on('error', (err) => {
113 |           reject(err);
114 |         });
115 |       });
116 | 
117 |     } catch (error) {
118 |       throw new Error(`Monica API request failed: ${error.message}`);
119 |     }
120 |   }
121 | }
122 | 
123 | /**
124 |  * Search using Monica AI
125 |  * @param {string} query - The search query
126 |  * @returns {Promise<string>} The search results
127 |  */
128 | export async function searchMonica(query) {
129 |   const client = new MonicaClient();
130 |   return await client.search(query);
131 | }
132 | 
```

--------------------------------------------------------------------------------
/bin/cli.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  5 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
  6 | 
  7 | // Import tool definitions and handlers
  8 | const modulePath = new URL('../src', import.meta.url).pathname;
  9 | 
 10 | // Dynamic imports
 11 | async function startServer() {
 12 |   try {
 13 |     // Dynamically import the modules
 14 |     const { searchToolDefinition, searchToolHandler } = await import(`${modulePath}/tools/searchTool.js`);
 15 |     const { iaskToolDefinition, iaskToolHandler } = await import(`${modulePath}/tools/iaskTool.js`);
 16 |     const { monicaToolDefinition, monicaToolHandler } = await import(`${modulePath}/tools/monicaTool.js`);
 17 | 
 18 |     // Create the MCP server
 19 |     const server = new Server({
 20 |       id: 'ddg-search-mcp',
 21 |       name: 'DuckDuckGo, IAsk AI & Monica Search MCP',
 22 |       description: 'A Model Context Protocol server for web search using DuckDuckGo, IAsk AI and Monica',
 23 |       version: '1.1.8'
 24 |     }, {
 25 |       capabilities: {
 26 |         tools: {
 27 |           listChanged: true
 28 |         }
 29 |       }
 30 |     });
 31 | 
 32 |     // Global variable to track available tools
 33 |     let availableTools = [
 34 |       searchToolDefinition,
 35 |       iaskToolDefinition,
 36 |       monicaToolDefinition
 37 |     ];
 38 | 
 39 |     // Define available tools
 40 |     server.setRequestHandler(ListToolsRequestSchema, async () => {
 41 |       return {
 42 |         tools: availableTools
 43 |       };
 44 |     });
 45 | 
 46 |     // Function to notify clients when tools list changes
 47 |     function notifyToolsChanged() {
 48 |       server.notification({
 49 |         method: 'notifications/tools/list_changed'
 50 |       });
 51 |     }
 52 | 
 53 |     // Handle tool execution
 54 |     server.setRequestHandler(CallToolRequestSchema, async (request) => {
 55 |       try {
 56 |         const { name, arguments: args } = request.params;
 57 |         
 58 |         // Validate tool name
 59 |         const validTools = ['web-search', 'iask-search', 'monica-search'];
 60 |         if (!validTools.includes(name)) {
 61 |           throw new Error(`Unknown tool: ${name}`);
 62 |         }
 63 | 
 64 |         // Route to the appropriate tool handler
 65 |         switch (name) {
 66 |           case 'web-search':
 67 |             return await searchToolHandler(args);
 68 | 
 69 |           case 'iask-search':
 70 |             return await iaskToolHandler(args);
 71 | 
 72 |           case 'monica-search':
 73 |             return await monicaToolHandler(args);
 74 | 
 75 |           default:
 76 |             throw new Error(`Tool not found: ${name}`);
 77 |         }
 78 |       } catch (error) {
 79 |         console.error(`Error handling ${request.params.name} tool call:`, error);
 80 |         
 81 |         // Return proper tool execution error format
 82 |         return {
 83 |           isError: true,
 84 |           content: [
 85 |             {
 86 |               type: 'text',
 87 |               text: `Error executing tool '${request.params.name}': ${error.message}`
 88 |             }
 89 |           ]
 90 |         };
 91 |       }
 92 |     });    // Display promotional message
 93 |     console.error('\n\x1b[36m╔════════════════════════════════════════════════════════════╗');
 94 |     console.error('║                                                            ║');
 95 |     console.error('║  \x1b[1m\x1b[31mDuckDuckGo & IAsk AI Search MCP\x1b[0m\x1b[36m by \x1b[1m\x1b[33m@OEvortex\x1b[0m\x1b[36m       ║');
 96 |     console.error('║                                                            ║');
 97 |     console.error('║  \x1b[0m👉 Subscribe to \x1b[1m\x1b[37myoutube.com/@OEvortex\x1b[0m\x1b[36m for more tools!  ║');
 98 |     console.error('║                                                            ║');
 99 |     console.error('╚════════════════════════════════════════════════════════════╝\x1b[0m\n');
100 | 
101 |     // Start the server with stdio transport
102 |     const transport = new StdioServerTransport();
103 |     await server.connect(transport);
104 |     console.error('DuckDuckGo, IAsk AI & Monica Search MCP server started and listening on stdio');
105 |   } catch (error) {
106 |     console.error('Failed to start server:', error);
107 |     process.exit(1);
108 |   }
109 | }
110 | 
111 | // Parse command line arguments
112 | const args = process.argv.slice(2);
113 | const helpFlag = args.includes('--help') || args.includes('-h');
114 | const versionFlag = args.includes('--version') || args.includes('-v');
115 | 
116 | if (helpFlag) {
117 |   console.log(`
118 | DuckDuckGo, IAsk AI & Monica Search MCP - A Model Context Protocol server for web search
119 | 
120 | Usage:
121 |   npx -y @oevortex/ddg_search@latest [options]
122 | 
123 | Options:
124 |   -h, --help     Show this help message
125 |   -v, --version  Show version information
126 | 
127 | This MCP server provides the following tools:
128 |   - web-search: Search the web using DuckDuckGo
129 |   - iask-search: Search using IAsk AI for AI-generated responses
130 |   - monica-search: Search using Monica AI for AI-generated responses
131 | 
132 | Created by @OEvortex
133 | Subscribe to youtube.com/@OEvortex for more tools and tutorials!
134 | 
135 | For more information, visit: https://github.com/OEvortex/ddg_search
136 |   `);
137 |   process.exit(0);
138 | }
139 | 
140 | if (versionFlag) {
141 |   // Read version from package.json using fs
142 |   import('fs/promises')
143 |     .then(async ({ readFile }) => {
144 |       try {
145 |         const packageJson = JSON.parse(
146 |           await readFile(new URL('../package.json', import.meta.url), 'utf8')
147 |         );
148 |         console.log(`DuckDuckGo & IAsk AI Search MCP v${packageJson.version}\nCreated by @OEvortex - Subscribe to youtube.com/@OEvortex!`);
149 |         process.exit(0);
150 |       } catch (err) {
151 |         console.error('Error reading version information:', err);
152 |         process.exit(1);
153 |       }
154 |     })
155 |     .catch(err => {
156 |       console.error('Error importing fs module:', err);
157 |       process.exit(1);
158 |     });
159 | } else {
160 |   // Start the server
161 |   startServer();
162 | }
163 | 
```

--------------------------------------------------------------------------------
/src/utils/search.js:
--------------------------------------------------------------------------------

```javascript
  1 | import axios from 'axios';
  2 | import * as cheerio from 'cheerio';
  3 | import https from 'https';
  4 | import { getRandomUserAgent } from './user_agents.js';
  5 | 
  6 | // Constants
  7 | const MAX_CACHE_PAGES = 5;
  8 | 
  9 | // Cache results to avoid repeated requests
 10 | const resultsCache = new Map();
 11 | const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
 12 | 
 13 | // HTTPS agent configuration to handle certificate chain issues
 14 | const httpsAgent = new https.Agent({
 15 |   rejectUnauthorized: true, // Keep security enabled
 16 |   keepAlive: true,
 17 |   timeout: 10000,
 18 |   // Provide fallback for certificate issues while maintaining security
 19 |   secureProtocol: 'TLSv1_2_method'
 20 | });
 21 | 
 22 | /**
 23 |  * Generate a cache key for a search query
 24 |  * @param {string} query - The search query
 25 |  * @returns {string} The cache key
 26 |  */
 27 | function getCacheKey(query) {
 28 |   return `${query}`;
 29 | }
 30 | 
 31 | /**
 32 |  * Clear old entries from the cache
 33 |  */
 34 | function clearOldCache() {
 35 |   const now = Date.now();
 36 |   for (const [key, value] of resultsCache.entries()) {
 37 |     if (now - value.timestamp > CACHE_DURATION) {
 38 |       resultsCache.delete(key);
 39 |     }
 40 |   }
 41 | }
 42 | 
 43 | /**
 44 |  * Extract the direct URL from a DuckDuckGo redirect URL
 45 |  * @param {string} duckduckgoUrl - The DuckDuckGo URL to extract from
 46 |  * @returns {string} The direct URL
 47 |  */
 48 | function extractDirectUrl(duckduckgoUrl) {
 49 |   try {
 50 |     // Handle relative URLs from DuckDuckGo
 51 |     if (duckduckgoUrl.startsWith('//')) {
 52 |       duckduckgoUrl = 'https:' + duckduckgoUrl;
 53 |     } else if (duckduckgoUrl.startsWith('/')) {
 54 |       duckduckgoUrl = 'https://duckduckgo.com' + duckduckgoUrl;
 55 |     }
 56 | 
 57 |     const url = new URL(duckduckgoUrl);
 58 | 
 59 |     // Extract direct URL from DuckDuckGo redirect
 60 |     if (url.hostname === 'duckduckgo.com' && url.pathname === '/l/') {
 61 |       const uddg = url.searchParams.get('uddg');
 62 |       if (uddg) {
 63 |         return decodeURIComponent(uddg);
 64 |       }
 65 |     }
 66 | 
 67 |     // Handle ad redirects
 68 |     if (url.hostname === 'duckduckgo.com' && url.pathname === '/y.js') {
 69 |       const u3 = url.searchParams.get('u3');
 70 |       if (u3) {
 71 |         try {
 72 |           const decodedU3 = decodeURIComponent(u3);
 73 |           const u3Url = new URL(decodedU3);
 74 |           const clickUrl = u3Url.searchParams.get('ld');
 75 |           if (clickUrl) {
 76 |             return decodeURIComponent(clickUrl);
 77 |           }
 78 |           return decodedU3;
 79 |         } catch {
 80 |           return duckduckgoUrl;
 81 |         }
 82 |       }
 83 |     }
 84 | 
 85 |     return duckduckgoUrl;
 86 |   } catch {
 87 |     // If URL parsing fails, try to extract URL from a basic string match
 88 |     const urlMatch = duckduckgoUrl.match(/https?:\/\/[^\s<>"]+/);
 89 |     if (urlMatch) {
 90 |       return urlMatch[0];
 91 |     }
 92 |     return duckduckgoUrl;
 93 |   }
 94 | }
 95 | 
 96 | /**
 97 |  * Get a favicon URL for a given website URL
 98 |  * @param {string} url - The website URL
 99 |  * @returns {string} The favicon URL
100 |  */
101 | function getFaviconUrl(url) {
102 |   try {
103 |     const urlObj = new URL(url);
104 |     return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`;
105 |   } catch {
106 |     return ''; // Return empty string if URL is invalid
107 |   }
108 | }
109 | 
110 | 
111 | /**
112 |  * Scrapes search results from DuckDuckGo HTML
113 |  * @param {string} query - The search query
114 |  * @param {number} numResults - Number of results to return (default: 10)
115 |  * @returns {Promise<Array>} - Array of search results
116 |  */
117 | async function searchDuckDuckGo(query, numResults = 10, mode = 'short') {
118 |   try {
119 |     // Clear old cache entries
120 |     clearOldCache();
121 | 
122 |     // Check cache first
123 |     const cacheKey = getCacheKey(query);
124 |     const cachedResults = resultsCache.get(cacheKey);
125 | 
126 |     if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
127 |       return cachedResults.results.slice(0, numResults);
128 |     }
129 | 
130 |     // Get a random user agent
131 |     const userAgent = getRandomUserAgent();
132 | 
133 |     // Fetch results
134 |     const response = await axios.get(
135 |       `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
136 |       {
137 |         headers: {
138 |           'User-Agent': userAgent
139 |         },
140 |         httpsAgent: httpsAgent
141 |       }
142 |     );
143 | 
144 |     if (response.status !== 200) {
145 |       throw new Error('Failed to fetch search results');
146 |     }
147 | 
148 |     const html = response.data;
149 | 
150 |     // Parse results using cheerio
151 |     const $ = cheerio.load(html);
152 | 
153 |     const results = [];
154 |     const jinaFetchPromises = [];
155 |     $('.result').each((i, result) => {
156 |       const $result = $(result);
157 |       const titleEl = $result.find('.result__title a');
158 |       const linkEl = $result.find('.result__url');
159 |       const snippetEl = $result.find('.result__snippet');
160 | 
161 |       const title = titleEl.text()?.trim();
162 |       const rawLink = titleEl.attr('href');
163 |       const description = snippetEl.text()?.trim();
164 |       const displayUrl = linkEl.text()?.trim();
165 | 
166 |       const directLink = extractDirectUrl(rawLink || '');
167 |       const favicon = getFaviconUrl(directLink);
168 |       const jinaUrl = getJinaAiUrl(directLink);
169 | 
170 |       if (title && directLink) {
171 |         if (mode === 'detailed') {
172 |           jinaFetchPromises.push(
173 |             axios.get(jinaUrl, {
174 |               headers: {
175 |                 'User-Agent': getRandomUserAgent()
176 |               },
177 |               httpsAgent: httpsAgent,
178 |               timeout: 10000
179 |             })
180 |             .then(jinaRes => {
181 |               let jinaContent = '';
182 |               if (jinaRes.status === 200 && typeof jinaRes.data === 'string') {
183 |                 const $jina = cheerio.load(jinaRes.data);
184 |                 jinaContent = $jina('body').text()
185 |               }
186 |               return {
187 |                 title,
188 |                 url: directLink,
189 |                 snippet: description || '',
190 |                 favicon: favicon,
191 |                 displayUrl: displayUrl || '',
192 |                 Description: jinaContent
193 |               };
194 |             })
195 |             .catch(() => {
196 |               return {
197 |                 title,
198 |                 url: directLink,
199 |                 snippet: description || '',
200 |                 favicon: favicon,
201 |                 displayUrl: displayUrl || '',
202 |                 Description: ''
203 |               };
204 |             })
205 |           );
206 |         } else {
207 |           // short mode: omit Description
208 |           jinaFetchPromises.push(
209 |             Promise.resolve({
210 |               title,
211 |               url: directLink,
212 |               snippet: description || '',
213 |               favicon: favicon,
214 |               displayUrl: displayUrl || ''
215 |             })
216 |           );
217 |         }
218 |       }
219 |     });
220 | 
221 |     // Wait for all Jina AI fetches to complete
222 |     const jinaResults = await Promise.all(jinaFetchPromises);
223 |     results.push(...jinaResults);
224 | 
225 |     // Get limited results
226 |     const limitedResults = results.slice(0, numResults);
227 | 
228 |     // Cache the results
229 |     resultsCache.set(cacheKey, {
230 |       results: limitedResults,
231 |       timestamp: Date.now()
232 |     });
233 | 
234 |     // If cache is too big, remove oldest entries
235 |     if (resultsCache.size > MAX_CACHE_PAGES) {
236 |       const oldestKey = Array.from(resultsCache.keys())[0];
237 |       resultsCache.delete(oldestKey);
238 |     }
239 | 
240 |     return limitedResults;
241 |   } catch (error) {
242 |     console.error('Error searching DuckDuckGo:', error.message);
243 |     throw error;
244 |   }
245 | }
246 | 
247 | 
248 | export {
249 |   searchDuckDuckGo,
250 |   extractDirectUrl,
251 |   getFaviconUrl
252 | };
253 | 
254 | /**
255 |  * Generate a Jina AI URL for a given website URL
256 |  * @param {string} url - The website URL
257 |  * @returns {string} The Jina AI URL
258 |  */
259 | function getJinaAiUrl(url) {
260 |   try {
261 |     const urlObj = new URL(url);
262 |     return `https://r.jina.ai/${urlObj.href}`;
263 |   } catch {
264 |     return '';
265 |   }
266 | }
267 | 
268 | export { getJinaAiUrl };
269 | 
```

--------------------------------------------------------------------------------
/src/utils/search_iask.js:
--------------------------------------------------------------------------------

```javascript
  1 | import axios from 'axios';
  2 | import WebSocket from 'ws';
  3 | import * as cheerio from 'cheerio';
  4 | import TurndownService from 'turndown';
  5 | import * as tough from 'tough-cookie';
  6 | import { wrapper } from 'axios-cookiejar-support';
  7 | import { getRandomUserAgent } from './user_agents.js';
  8 | 
  9 | const { CookieJar } = tough;
 10 | 
 11 | // Valid modes and detail levels
 12 | const VALID_MODES = ['question', 'academic', 'forums', 'wiki', 'thinking'];
 13 | const VALID_DETAIL_LEVELS = ['concise', 'detailed', 'comprehensive'];
 14 | 
 15 | // Cache results to avoid repeated requests
 16 | const resultsCache = new Map();
 17 | const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
 18 | 
 19 | const DEFAULT_TIMEOUT = 30000;
 20 | const API_ENDPOINT = 'https://iask.ai/';
 21 | 
 22 | /**
 23 |  * Generate a cache key for a search query
 24 |  * @param {string} query - The search query
 25 |  * @param {string} mode - The search mode
 26 |  * @param {string|null} detailLevel - The detail level
 27 |  * @returns {string} The cache key
 28 |  */
 29 | function getCacheKey(query, mode, detailLevel) {
 30 |   return `iask-${mode}-${detailLevel || 'default'}-${query}`;
 31 | }
 32 | 
 33 | /**
 34 |  * Clear old entries from the cache
 35 |  */
 36 | function clearOldCache() {
 37 |   const now = Date.now();
 38 |   for (const [key, value] of resultsCache.entries()) {
 39 |     if (now - value.timestamp > CACHE_DURATION) {
 40 |       resultsCache.delete(key);
 41 |     }
 42 |   }
 43 | }
 44 | 
 45 | /**
 46 |  * Recursively search for cached HTML content in diff object
 47 |  * @param {any} diff - The diff object to search
 48 |  * @returns {string|null} The found content or null
 49 |  */
 50 | function cacheFind(diff) {
 51 |   const values = Array.isArray(diff) ? diff : Object.values(diff);
 52 |   
 53 |   for (const value of values) {
 54 |     if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
 55 |       const cache = cacheFind(value);
 56 |       if (cache) return cache;
 57 |     }
 58 |     
 59 |     if (typeof value === 'string' && /<p>.+?<\/p>/.test(value)) {
 60 |       const turndownService = new TurndownService();
 61 |       return turndownService.turndown(value).trim();
 62 |     }
 63 |   }
 64 |   
 65 |   return null;
 66 | }
 67 | 
 68 | /**
 69 |  * Format HTML content into readable markdown text
 70 |  * @param {string} htmlContent - The HTML content to format
 71 |  * @returns {string} Formatted text
 72 |  */
 73 | function formatHtml(htmlContent) {
 74 |   if (!htmlContent) return '';
 75 |   
 76 |   const $ = cheerio.load(htmlContent);
 77 |   const outputLines = [];
 78 | 
 79 |   $('h1, h2, h3, p, ol, ul, div').each((_, element) => {
 80 |     const tagName = element.tagName.toLowerCase();
 81 |     const $el = $(element);
 82 | 
 83 |     if (['h1', 'h2', 'h3'].includes(tagName)) {
 84 |       outputLines.push(`\n**${$el.text().trim()}**\n`);
 85 |     } else if (tagName === 'p') {
 86 |       let text = $el.text().trim();
 87 |       // Remove IAsk attribution
 88 |       text = text.replace(/^According to Ask AI & Question AI www\.iAsk\.ai:\s*/i, '').trim();
 89 |       // Remove footnote markers
 90 |       text = text.replace(/\[\d+\]\(#fn:\d+ 'see footnote'\)/g, '');
 91 |       if (text) outputLines.push(text + '\n');
 92 |     } else if (['ol', 'ul'].includes(tagName)) {
 93 |       $el.find('li').each((_, li) => {
 94 |         outputLines.push('- ' + $(li).text().trim() + '\n');
 95 |       });
 96 |     } else if (tagName === 'div' && $el.hasClass('footnotes')) {
 97 |       outputLines.push('\n**Authoritative Sources**\n');
 98 |       $el.find('li').each((_, li) => {
 99 |         const link = $(li).find('a');
100 |         if (link.length) {
101 |           outputLines.push(`- ${link.text().trim()} (${link.attr('href')})\n`);
102 |         }
103 |       });
104 |     }
105 |   });
106 | 
107 |   return outputLines.join('');
108 | }
109 | 
110 | /**
111 |  * Search using IAsk AI via WebSocket (Phoenix LiveView)
112 |  * @param {string} prompt - The search query or prompt
113 |  * @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
114 |  * @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
115 |  * @returns {Promise<string>} The search results
116 |  */
117 | async function searchIAsk(prompt, mode = 'thinking', detailLevel = null) {
118 |   // Validate mode
119 |   if (!VALID_MODES.includes(mode)) {
120 |     throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
121 |   }
122 | 
123 |   // Validate detail level
124 |   if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
125 |     throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
126 |   }
127 | 
128 |   // Clear old cache entries
129 |   clearOldCache();
130 | 
131 |   const cacheKey = getCacheKey(prompt, mode, detailLevel);
132 |   const cachedResults = resultsCache.get(cacheKey);
133 | 
134 |   if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
135 |     return cachedResults.results;
136 |   }
137 | 
138 |   // Build URL parameters
139 |   const params = new URLSearchParams({ mode, q: prompt });
140 |   if (detailLevel) {
141 |     params.append('options[detail_level]', detailLevel);
142 |   }
143 | 
144 |   // Create a cookie jar for session management
145 |   const jar = new CookieJar();
146 |   const client = wrapper(axios.create({ jar }));
147 | 
148 |   // Get initial page and extract tokens
149 |   const response = await client.get(API_ENDPOINT, {
150 |     params: Object.fromEntries(params),
151 |     timeout: DEFAULT_TIMEOUT,
152 |     headers: {
153 |       'User-Agent': getRandomUserAgent()
154 |     }
155 |   });
156 | 
157 |   const $ = cheerio.load(response.data);
158 |   
159 |   const phxNode = $('[id^="phx-"]').first();
160 |   const csrfToken = $('[name="csrf-token"]').attr('content');
161 |   const phxId = phxNode.attr('id');
162 |   const phxSession = phxNode.attr('data-phx-session');
163 | 
164 |   if (!phxId || !csrfToken) {
165 |     throw new Error('Failed to extract required tokens from page');
166 |   }
167 | 
168 |   // Get the actual response URL (after any redirects)
169 |   const responseUrl = response.request.res?.responseUrl || response.config.url;
170 |   
171 |   // Get cookies from the jar for WebSocket connection
172 |   const cookies = await jar.getCookies(API_ENDPOINT);
173 |   const cookieString = cookies.map(c => `${c.key}=${c.value}`).join('; ');
174 |   
175 |   // Build WebSocket URL
176 |   const wsParams = new URLSearchParams({
177 |     '_csrf_token': csrfToken,
178 |     'vsn': '2.0.0'
179 |   });
180 |   const wsUrl = `wss://iask.ai/live/websocket?${wsParams.toString()}`;
181 | 
182 |   return new Promise((resolve, reject) => {
183 |     const ws = new WebSocket(wsUrl, {
184 |       headers: {
185 |         'Cookie': cookieString,
186 |         'User-Agent': getRandomUserAgent(),
187 |         'Origin': 'https://iask.ai'
188 |       }
189 |     });
190 |     
191 |     let buffer = '';
192 |     let timeoutId;
193 | 
194 |     ws.on('open', () => {
195 |       // Send phx_join message
196 |       ws.send(JSON.stringify([
197 |         null,
198 |         null,
199 |         `lv:${phxId}`,
200 |         'phx_join',
201 |         {
202 |           params: { _csrf_token: csrfToken },
203 |           url: responseUrl,
204 |           session: phxSession
205 |         }
206 |       ]));
207 |     });
208 | 
209 |     ws.on('message', (data) => {
210 |       try {
211 |         const msg = JSON.parse(data.toString());
212 |         if (!msg) return;
213 | 
214 |         const diff = msg[4];
215 |         if (!diff) return;
216 | 
217 |         let chunk = null;
218 | 
219 |         try {
220 |           // Try to get chunk from diff.e[0][1].data
221 |           // Use non-optional chaining to trigger exception if path doesn't exist
222 |           if (diff.e) {
223 |             chunk = diff.e[0][1].data;
224 |             
225 |             if (chunk) {
226 |               let formatted;
227 |               if (/<[^>]+>/.test(chunk)) {
228 |                 formatted = formatHtml(chunk);
229 |               } else {
230 |                 formatted = chunk.replace(/<br\/>/g, '\n');
231 |               }
232 |               
233 |               buffer += formatted;
234 |             }
235 |           } else {
236 |             throw new Error('No diff.e');
237 |           }
238 |         } catch {
239 |           // Fallback to cacheFind
240 |           const cache = cacheFind(diff);
241 |           if (cache) {
242 |             let formatted;
243 |             if (/<[^>]+>/.test(cache)) {
244 |               formatted = formatHtml(cache);
245 |             } else {
246 |               formatted = cache;
247 |             }
248 |             buffer += formatted;
249 |             // Close after cache find
250 |             ws.close();
251 |             return;
252 |           }
253 |         }
254 |       } catch (err) {
255 |         reject(new Error(`IAsk API error: ${err.message}`));
256 |         ws.close();
257 |       }
258 |     });
259 | 
260 |     ws.on('close', () => {
261 |       clearTimeout(timeoutId);
262 |       
263 |       // Cache the result
264 |       if (buffer) {
265 |         resultsCache.set(cacheKey, {
266 |           results: buffer,
267 |           timestamp: Date.now()
268 |         });
269 |       }
270 |       
271 |       resolve(buffer || 'No results found.');
272 |     });
273 | 
274 |     ws.on('error', (err) => {
275 |       clearTimeout(timeoutId);
276 |       reject(new Error(`WebSocket error: ${err.message}`));
277 |     });
278 | 
279 |     timeoutId = setTimeout(() => {
280 |       ws.close();
281 |     }, DEFAULT_TIMEOUT);
282 |   });
283 | }
284 | 
285 | export { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS };
286 | 
```