# 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 |
```