#
tokens: 13158/50000 19/19 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
node_modules
npm-debug.log
.dockerignore
.git
.gitignore
.smithery
dist
.build
.idea
.vscode
**/*.md
**/*.log

```

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

```
.qodo

# Dependency directories
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Build directories
dist/
build/

# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store

```

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

```
# Development files
.git/
.github/
.vscode/
.idea/
.DS_Store

# Test files
test/
tests/
__tests__/
coverage/

# Configuration files
.eslintrc*
.prettierrc*
.editorconfig
tsconfig.json
jest.config.js

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Misc
.qodo
.env
.env.*
node_modules/

```

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

```markdown
<div align="center">
  <a href="https://www.npmjs.com/package/@oevortex/ddg_search">
    <img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
  </a>
  <a href="https://github.com/OEvortex/ddg_search/blob/main/LICENSE">
    <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
  </a>
  <a href="https://youtube.com/@OEvortex">
    <img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
  </a>
  <h1>DuckDuckGo, IAsk AI & Monica Search MCP <span style="font-size:2.2rem;">🔍🧠</span></h1>
  <p style="font-size:1.15rem; max-width:600px; margin:0 auto;">
    <strong>Lightning-fast, privacy-first Model Context Protocol (MCP) server for web search and AI-powered answers.<br>
    Powered by DuckDuckGo, IAsk AI and Monica.</strong>
  </p>
  <a href="https://glama.ai/mcp/servers/@OEvortex/ddg_search">
    <img width="380" height="200" src="https://glama.ai/mcp/servers/@OEvortex/ddg_search/badge" alt="DuckDuckGo Search MCP server" />
  </a>
  <br>
  <a href="https://youtube.com/@OEvortex"><strong>Subscribe for updates & tutorials</strong></a>
</div>

---

> [!IMPORTANT]
> DuckDuckGo Search MCP supports the Model Context Protocol (MCP) standard, making it compatible with various AI assistants and tools.

---

## ✨ Features

<div style="display: flex; flex-wrap: wrap; gap: 1.5em; margin-bottom: 1.5em;">  <div><b>🌐 Web search</b> using DuckDuckGo HTML</div>
  <div><b>🧠 AI search</b> using IAsk AI & Monica</div>
  <div><b>⚡ Performance optimized</b> with caching</div>
  <div><b>🛡️ Security features</b> including rate limiting and rotating user agents</div>
  <div><b>🔌 MCP-compliant</b> server implementation</div>
  <div><b>🆓 No API keys required</b> - works out of the box</div>
</div>


> [!IMPORTANT]
> Unlike many search tools, this package performs actual web scraping rather than using limited APIs, giving you more comprehensive results.

---

## 🚀 Quick Start

<div style="background: #222; color: #fff; padding: 1.5em; border-radius: 8px; margin: 1.5em 0;">
<b>Run instantly with npx:</b>

```bash
npx -y @oevortex/ddg_search@latest
```
</div>


> [!TIP]
> This will download and run the latest version of the MCP server directly without installation – perfect for quick use with AI assistants.

---

## 🛠️ Installation Options

<details>
<summary><b>Global Installation (npm)</b></summary>

```bash
npm install -g @oevortex/ddg_search
```

Run globally:

```bash
ddg-search-mcp
```

</details>

<details>
<summary><b>Global Installation (Yarn)</b></summary>

```bash
yarn global add @oevortex/ddg_search
```

Run globally:

```bash
ddg-search-mcp
```

</details>

<details>
<summary><b>Global Installation (pnpm)</b></summary>

```bash
pnpm add -g @oevortex/ddg_search
```

Run globally:

```bash
ddg-search-mcp
```

</details>

<details>
<summary><b>Local Installation (Development)</b></summary>

```bash
git clone https://github.com/OEvortex/ddg_search.git
cd ddg_search
npm install
npm start
```

Or with Yarn:

```bash
yarn install
yarn start
```

Or with pnpm:

```bash
pnpm install
pnpm start
```

</details>

---

## 🧑‍💻 Command Line Options

```bash
npx -y @oevortex/ddg_search@latest --help
```

> [!TIP]
> Use the <code>--version</code> flag to check which version you're running.

---

## 🤖 Using with MCP Clients

> [!IMPORTANT]
> The most common way to use this tool is by integrating it with MCP-compatible AI assistants.

Add the server to your MCP client configuration:

```json
{
  "mcpServers": {
    "ddg-search": {
      "command": "npx",
      "args": ["-y", "@oevortex/ddg_search@latest"]
    }
  }
}
```

Or if installed globally:

```json
{
  "mcpServers": {
    "ddg-search": {
      "command": "ddg-search-mcp"
    }
  }
}
```

> [!TIP]
> After configuring, restart your MCP client to apply the changes.

---

## 🧰 Tools Overview

<div style="display: flex; flex-wrap: wrap; gap: 2.5em; margin: 1.5em 0;">
  <div style="margin-bottom: 1.5em;">
    <b>🔍 Web Search Tool</b><br/>
    <code>web-search</code><br/>
    <ul>
      <li><b>query</b> (string, required): The search query</li>
      <li><b>page</b> (integer, optional, default: 1): Page number</li>
      <li><b>numResults</b> (integer, optional, default: 10): Number of results (1-20)</li>
    </ul>
    <i>Example: Search the web for "climate change solutions"</i>
  </div>
  <div style="margin-bottom: 1.5em;">
    <b>🧠 IAsk AI Search Tool</b><br/>
    <code>iask-search</code><br/>
    <ul>
      <li><b>query</b> (string, required): The search query or question</li>
      <li><b>mode</b> (string, optional, default: "question"): Search mode - "question", "academic", "forums", "wiki", or "thinking"</li>
      <li><b>detailLevel</b> (string, optional): Response detail level - "concise", "detailed", or "comprehensive"</li>
    </ul>
    <i>Example: Search IAsk AI for "Explain quantum computing in simple terms"</i>
  </div>
  <div style="margin-bottom: 1.5em;">
    <b>🤖 Monica AI Search Tool</b><br/>
    <code>monica-search</code><br/>
    <ul>
      <li><b>query</b> (string, required): The search query or question</li>
    </ul>
    <i>Example: Search Monica AI for "Latest advancements in AI"</i>
  </div>
</div>

---

## 📁 Project Structure


```text
bin/              # Command-line interface
src/
  index.js        # Main entry point
  tools/          # Tool definitions and handlers
    searchTool.js
    iaskTool.js
    monicaTool.js
  utils/
    search.js     # Search and URL utilities
    user_agents.js
    search_monica.js
    search_iask.js # IAsk AI search utilities
package.json
README.md
```

---

## 🤝 Contributing


Contributions are welcome! Please open issues or submit pull requests.

> [!NOTE]
> Please follow the existing code style and add tests for new features.

---

## 📺 YouTube Channel


<div align="center">
  <a href="https://youtube.com/@OEvortex"><img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" /></a>
  <br/>
  <a href="https://youtube.com/@OEvortex">youtube.com/@OEvortex</a>
</div>

---

## 📄 License


Apache License 2.0

> [!NOTE]
> This project is licensed under the Apache License 2.0 – see the <a href="LICENSE">LICENSE</a> file for details.

---

<div align="center">
  <sub>Made with ❤️ by <a href="https://youtube.com/@OEvortex">@OEvortex</a></sub>
</div>

```

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

```yaml
runtime: typescript

build:
  external:
    - canvas
    - utf-8-validate
    - bufferutil
  esbuild:
    bundle: true
    platform: node
    format: cjs
    target: node18

```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "allowJs": true,
    "checkJs": false,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": false,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "types": ["node"]
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "bin"
  ]
}
```

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

```dockerfile
# Build stage: install dependencies
FROM node:22-slim AS build
WORKDIR /app

# Copy package manifests and lockfile first for better caching
COPY package.json package-lock.json ./

# Install production dependencies (use npm ci when lockfile exists)
RUN if [ -f package-lock.json ]; then npm ci --production; else npm install --production; fi

# Copy application source
COPY . .

# Final minimal runtime image
FROM node:22-slim AS runtime
WORKDIR /app

# Copy node_modules and built app from build stage
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app .

# Expose port in case the MCP server needs it
EXPOSE 3000

# Default command: use the CLI entry which starts the MCP server
CMD ["node", "bin/cli.js"]

```

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

```javascript
import { searchMonica } from '../utils/search_monica.js';

/**
 * Monica AI search tool definition
 */
export const monicaToolDefinition = {
  name: 'monica-search',
  title: 'Monica AI Search',
  description: 'AI-powered search using Monica AI. Returns AI-generated responses based on web content.',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: 'The search query or question.'
      }
    },
    required: ['query']
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: false
  }
};

/**
 * Monica AI search tool handler
 * @param {Object} params - The tool parameters
 * @returns {Promise<Object>} - The tool result
 */
export async function monicaToolHandler(params) {
  const { query } = params;
  
  console.log(`Searching Monica AI for: "${query}"`);
  
  try {
    const result = await searchMonica(query);
    return {
      content: [
        {
          type: 'text',
          text: result || 'No results found.'
        }
      ]
    };
  } catch (error) {
    console.error(`Error in Monica search: ${error.message}`);
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: `Error searching Monica: ${error.message}`
        }
      ]
    };
  }
}

```

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

```json
{
  "name": "@oevortex/ddg_search",
  "version": "1.1.8",
  "description": "A Model Context Protocol server for web search using DuckDuckGo and IAsk AI",
  "main": "src/index.js",
  "module": "src/index.ts",
  "exports": {
    ".": {
      "import": "./src/index.js",
      "default": "./src/index.js"
    }
  },
  "bin": {
    "ddg-search-mcp": "bin/cli.js",
    "oevortex-ddg-search": "bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node bin/cli.js",
    "prepublishOnly": "npm run lint",
    "lint": "echo \"No linting configured\"",
    "build": "npx @smithery/cli build",
    "dev": "npx @smithery/cli dev"
  },
  "publishConfig": {
    "access": "public"
  },
  "keywords": [
    "mcp",
    "model-context-protocol",
    "duckduckgo",
    "iask",
    "search",
    "web-search",
    "ai-search",
    "claude",
    "ai",
    "llm"
  ],
  "author": "OEvortex",
  "license": "Apache-2.0",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.17.4",
    "axios": "^1.8.4",
    "axios-cookiejar-support": "^6.0.5",
    "cheerio": "^1.0.0",
    "smithery": "^0.5.2",
    "tough-cookie": "^6.0.0",
    "turndown": "^7.2.2",
    "ws": "^8.18.3"
  },
  "devDependencies": {
    "@types/node": "^24.3.0",
    "tsx": "^4.20.4",
    "typescript": "^5.9.2"
  }
}
```

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

```javascript
import { searchDuckDuckGo } from '../utils/search.js';

/**
 * Web search tool definition
 */
export const searchToolDefinition = {
  name: 'web-search',
  title: 'Web Search',
  description: 'Perform a web search using DuckDuckGo and receive detailed results including titles, URLs, and summaries.',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: 'Enter your search query to find the most relevant web pages.'
      },
      numResults: {
        type: 'integer',
        description: 'Specify how many results to display (default: 3, maximum: 20).',
        default: 3,
        minimum: 1,
        maximum: 20
      },
      mode: {
        type: 'string',
        description: "Choose 'short' for basic results (no Description) or 'detailed' for full results (includes Description).",
        enum: ['short', 'detailed'],
        default: 'short'
      }
    },
    required: ['query']
  }
};

/**
 * Web search tool handler
 * @param {Object} params - The tool parameters
 * @returns {Promise<Object>} - The tool result
 */
export async function searchToolHandler(params) {
  const { query, numResults = 3, mode = 'short' } = params;
  console.log(`Searching for: ${query} (${numResults} results, mode: ${mode})`);

  const results = await searchDuckDuckGo(query, numResults, mode);
  console.log(`Found ${results.length} results`);

  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(results)
      }
    ]
  };
}

```

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

```markdown
# Changelog

All notable changes to this project will be documented in this file.
## [1.1.8] - 2025-12-03
### Added
- Added new `getRandomUserAgent` function to rotate user agents
- Added new `src/utils/user_agents.js` file containing list of user agents
- switch to use `user_agents.js` file for user agent rotation
- Removed stream from iaskTool.js & search_iask.js
- Added new `monica-search` tool for AI-powered search using Monica AI

### Changed
- Updated `src/index.ts` to use IAsk tool instead of Felo tool
- Updated `package.json` description, keywords, and dependencies (`turndown`, `ws`)
- Updated `README.md` to reference IAsk AI and document new tool parameters
- Removed old Felo tool files (`feloTool.js`, `search_felo.js`)

## [1.1.7] - 2025-11-30
### Changed
- Replaced Felo AI tool with IAsk AI tool for advanced AI-powered search
- Added new dependencies: `turndown` for HTML to Markdown conversion, `ws` for WebSocket support
- Updated README to reflect changes and new tool usage
- Added new modes: 'short', 'detailed' in web search tool
- Added `src/utils/search_iask.js` implementing IAsk API client
- Added `src/tools/iaskTool.js` tool definition and handler
- Updated `src/index.ts` to use IAsk tool instead of Felo
- Updated `package.json` description, keywords, and dependencies (`turndown`, `ws`)
- Updated `README.md` to reference IAsk AI and document new tool parameters
- Removed old Felo tool files (`feloTool.js`, `search_felo.js`)

## [1.1.2] - 2025-11-29
### Added
- Initial release with DuckDuckGo and Felo AI search tools
- MCP server implementation
- Caching, rotating user agents, and web scraping features

```

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

```javascript
/**
 * List of user agents for rotation
 */
const USER_AGENTS = [
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
  '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',
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  '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',
  'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
  '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',
  'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
  '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',
  'Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36',
  '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',
  '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',
  'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0',
];

/**
 * Get a random user agent from the list
 * @returns {string} A random user agent string
 */
export function getRandomUserAgent() {
  return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
}

```

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

```javascript
import { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS } from '../utils/search_iask.js';

/**
 * IAsk AI search tool definition
 */
export const iaskToolDefinition = {
  name: 'iask-search',
  title: 'IAsk AI Search',
  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.',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: 'The search query or question to ask. Supports natural language questions for comprehensive AI-generated responses.'
      },
      mode: {
        type: 'string',
        description: 'Search mode to use. Options: "question" (general questions), "academic" (scholarly/research), "forums" (community discussions), "wiki" (encyclopedia-style), "thinking" (deep analysis). Default is "question".',
        enum: VALID_MODES,
        default: 'question'
      },
      detailLevel: {
        type: 'string',
        description: 'Level of detail in the response. Options: "concise" (brief), "detailed" (moderate), "comprehensive" (extensive). Default is null (standard response).',
        enum: VALID_DETAIL_LEVELS
      }
    },
    required: ['query']
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: false
  }
};

/**
 * IAsk AI search tool handler
 * @param {Object} params - The tool parameters
 * @returns {Promise<Object>} - The tool result
 */
export async function iaskToolHandler(params) {
  const { 
    query, 
    mode = 'thinking', 
    detailLevel = null
  } = params;
  
  console.log(`Searching IAsk AI for: "${query}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'})`);
  
  try {
    const response = await searchIAsk(query, mode, detailLevel);
    
    return {
      content: [
        {
          type: 'text',
          text: response || 'No results found.'
        }
      ]
    };
  } catch (error) {
    console.error(`Error in IAsk search: ${error.message}`);
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: `Error searching IAsk: ${error.message}`
        }
      ]
    };
  }
}

```

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

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

// Import tool definitions and handlers
import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
import { iaskToolDefinition, iaskToolHandler } from './tools/iaskTool.js';
import { monicaToolDefinition, monicaToolHandler } from './tools/monicaTool.js';

// Required: Export default createServer function for Smithery
export default function createServer({ config }: { config?: any } = {}) {
  console.log('Creating MCP server with latest SDK...');

  // Global variable to track available tools
  const availableTools = [
    searchToolDefinition,
    iaskToolDefinition,
    monicaToolDefinition
  ];
  
  console.log('Available tools:', availableTools.map(t => t.name));

  // Create the MCP server using the Server class
  const server = new Server({
    name: 'ddg-search-mcp',
    version: '1.1.2'
  }, {
    capabilities: {
      tools: {
        listChanged: true
      }
    }
  });

  // Define available tools
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    console.log('Tools list requested, returning:', availableTools.length, 'tools');
    return {
      tools: availableTools
    };
  });

  // Handle tool execution
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    try {
      const { name, arguments: args } = request.params;
      console.log(`Tool call received: ${name} with args:`, args);
      
      // Route to the appropriate tool handler
      switch (name) {
        case 'web-search':
          return await searchToolHandler(args);

        case 'iask-search':
          return await iaskToolHandler(args);

        case 'monica-search':
          return await monicaToolHandler(args);

        default:
          throw new Error(`Tool not found: ${name}`);
      }
    } catch (error: any) {
      console.error(`Error handling ${request.params.name} tool call:`, error);
      
      // Return proper tool execution error format
      return {
        isError: true,
        content: [
          {
            type: 'text',
            text: `Error executing tool '${request.params.name}': ${error.message}`
          }
        ]
      };
    }
  });

  console.log('MCP server created successfully');
  
  // Return the server instance (required for Smithery)
  return server;
}

// Optional: No configuration schema needed for this server
// export const configSchema = z.object({});
```

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

```javascript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

// Import tool definitions and handlers
import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
import { iaskToolDefinition, iaskToolHandler } from './tools/iaskTool.js';
import { monicaToolDefinition, monicaToolHandler } from './tools/monicaTool.js';

// Required: Export default createServer function for Smithery
export default function createServer({ config } = {}) {
  console.log('Creating MCP server with latest SDK...');

  // Global variable to track available tools
  const availableTools = [
    searchToolDefinition,
    iaskToolDefinition,
    monicaToolDefinition
  ];
  
  console.log('Available tools:', availableTools.map(t => t.name));

  // Create the MCP server using the Server class
  const server = new Server({
    name: 'ddg-search-mcp',
    version: '1.1.2'
  }, {
    capabilities: {
      tools: {
        listChanged: true
      }
    }
  });

  // Define available tools
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    console.log('Tools list requested, returning:', availableTools.length, 'tools');
    return {
      tools: availableTools
    };
  });

  // Handle tool execution
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    try {
      const { name, arguments: args } = request.params;
      console.log(`Tool call received: ${name} with args:`, args);
      
      // Route to the appropriate tool handler
      switch (name) {
        case 'web-search':
          return await searchToolHandler(args);

        case 'iask-search':
          return await iaskToolHandler(args);

        case 'monica-search':
          return await monicaToolHandler(args);

        default:
          throw new Error(`Tool not found: ${name}`);
      }
    } catch (error) {
      console.error(`Error handling ${request.params.name} tool call:`, error);
      
      // Return proper tool execution error format
      return {
        isError: true,
        content: [
          {
            type: 'text',
            text: `Error executing tool '${request.params.name}': ${error.message}`
          }
        ]
      };
    }
  });

  console.log('MCP server created successfully');
  
  // Return the server instance (required for Smithery)
  return server;
}

// Legacy standalone server support (for CLI usage)
if (import.meta.url === `file://${process.argv[1]}`) {
  async function main() {
    try {
      const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
      const server = createServer();
      const transport = new StdioServerTransport();
      await server.connect(transport);
      console.error('WebSearch MCP server started and listening on stdio');
    } catch (error) {
      console.error('Failed to start server:', error);
      process.exit(1);
    }
  }

  main();
}

```

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

```javascript
import axios from 'axios';
import { randomUUID } from 'crypto';
import { getRandomUserAgent } from './user_agents.js';

class MonicaClient {
  constructor(timeout = 60000) {
    this.apiEndpoint = "https://monica.so/api/search_v1/search";
    this.timeout = timeout;
    this.clientId = randomUUID();
    this.sessionId = "";
    
    this.headers = {
      "accept": "*/*",
      "accept-encoding": "gzip, deflate, br, zstd",
      "accept-language": "en-US,en;q=0.9",
      "content-type": "application/json",
      "dnt": "1",
      "origin": "https://monica.so",
      "referer": "https://monica.so/answers",
      "sec-ch-ua": '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
      "sec-ch-ua-mobile": "?0",
      "sec-ch-ua-platform": '"Windows"',
      "sec-fetch-dest": "empty",
      "sec-fetch-mode": "cors",
      "sec-fetch-site": "same-origin",
      "sec-gpc": "1",
      "user-agent": getRandomUserAgent(),
      "x-client-id": this.clientId,
      "x-client-locale": "en",
      "x-client-type": "web",
      "x-client-version": "5.4.3",
      "x-from-channel": "NA",
      "x-product-name": "Monica-Search",
      "x-time-zone": "Asia/Calcutta;-330"
    };

    // Axios instance
    this.client = axios.create({
      headers: this.headers,
      timeout: this.timeout,
      withCredentials: true
    });
  }

  formatResponse(text) {
    // Clean up markdown formatting
    let cleanedText = text.replace(/\*\*/g, '');
    
    // Remove any empty lines
    cleanedText = cleanedText.replace(/\n\s*\n/g, '\n\n');
    
    // Remove any trailing whitespace
    return cleanedText.trim();
  }

  async search(prompt) {
    const taskId = randomUUID();
    const payload = {
      "pro": false,
      "query": prompt,
      "round": 1,
      "session_id": this.sessionId,
      "language": "auto",
      "task_id": taskId
    };

    const cookies = {
      "monica_home_theme": "auto"
    };
    
    // Convert cookies object to string
    const cookieString = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');

    try {
      const response = await this.client.post(this.apiEndpoint, payload, {
        headers: {
          ...this.headers,
          'Cookie': cookieString
        },
        responseType: 'stream'
      });

      let fullText = '';
      
      return new Promise((resolve, reject) => {
        response.data.on('data', (chunk) => {
          const lines = chunk.toString().split('\n');
          for (const line of lines) {
            if (line.startsWith('data: ')) {
              try {
                const jsonStr = line.substring(6);
                const data = JSON.parse(jsonStr);

                if (data.session_id) {
                  this.sessionId = data.session_id;
                }

                if (data.text) {
                  fullText += data.text;
                }
              } catch (e) {
                // Ignore parse errors
              }
            }
          }
        });

        response.data.on('end', () => {
          resolve(this.formatResponse(fullText));
        });

        response.data.on('error', (err) => {
          reject(err);
        });
      });

    } catch (error) {
      throw new Error(`Monica API request failed: ${error.message}`);
    }
  }
}

/**
 * Search using Monica AI
 * @param {string} query - The search query
 * @returns {Promise<string>} The search results
 */
export async function searchMonica(query) {
  const client = new MonicaClient();
  return await client.search(query);
}

```

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

```javascript
#!/usr/bin/env node

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

// Import tool definitions and handlers
const modulePath = new URL('../src', import.meta.url).pathname;

// Dynamic imports
async function startServer() {
  try {
    // Dynamically import the modules
    const { searchToolDefinition, searchToolHandler } = await import(`${modulePath}/tools/searchTool.js`);
    const { iaskToolDefinition, iaskToolHandler } = await import(`${modulePath}/tools/iaskTool.js`);
    const { monicaToolDefinition, monicaToolHandler } = await import(`${modulePath}/tools/monicaTool.js`);

    // Create the MCP server
    const server = new Server({
      id: 'ddg-search-mcp',
      name: 'DuckDuckGo, IAsk AI & Monica Search MCP',
      description: 'A Model Context Protocol server for web search using DuckDuckGo, IAsk AI and Monica',
      version: '1.1.8'
    }, {
      capabilities: {
        tools: {
          listChanged: true
        }
      }
    });

    // Global variable to track available tools
    let availableTools = [
      searchToolDefinition,
      iaskToolDefinition,
      monicaToolDefinition
    ];

    // Define available tools
    server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: availableTools
      };
    });

    // Function to notify clients when tools list changes
    function notifyToolsChanged() {
      server.notification({
        method: 'notifications/tools/list_changed'
      });
    }

    // Handle tool execution
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
      try {
        const { name, arguments: args } = request.params;
        
        // Validate tool name
        const validTools = ['web-search', 'iask-search', 'monica-search'];
        if (!validTools.includes(name)) {
          throw new Error(`Unknown tool: ${name}`);
        }

        // Route to the appropriate tool handler
        switch (name) {
          case 'web-search':
            return await searchToolHandler(args);

          case 'iask-search':
            return await iaskToolHandler(args);

          case 'monica-search':
            return await monicaToolHandler(args);

          default:
            throw new Error(`Tool not found: ${name}`);
        }
      } catch (error) {
        console.error(`Error handling ${request.params.name} tool call:`, error);
        
        // Return proper tool execution error format
        return {
          isError: true,
          content: [
            {
              type: 'text',
              text: `Error executing tool '${request.params.name}': ${error.message}`
            }
          ]
        };
      }
    });    // Display promotional message
    console.error('\n\x1b[36m╔════════════════════════════════════════════════════════════╗');
    console.error('║                                                            ║');
    console.error('║  \x1b[1m\x1b[31mDuckDuckGo & IAsk AI Search MCP\x1b[0m\x1b[36m by \x1b[1m\x1b[33m@OEvortex\x1b[0m\x1b[36m       ║');
    console.error('║                                                            ║');
    console.error('║  \x1b[0m👉 Subscribe to \x1b[1m\x1b[37myoutube.com/@OEvortex\x1b[0m\x1b[36m for more tools!  ║');
    console.error('║                                                            ║');
    console.error('╚════════════════════════════════════════════════════════════╝\x1b[0m\n');

    // Start the server with stdio transport
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error('DuckDuckGo, IAsk AI & Monica Search MCP server started and listening on stdio');
  } catch (error) {
    console.error('Failed to start server:', error);
    process.exit(1);
  }
}

// Parse command line arguments
const args = process.argv.slice(2);
const helpFlag = args.includes('--help') || args.includes('-h');
const versionFlag = args.includes('--version') || args.includes('-v');

if (helpFlag) {
  console.log(`
DuckDuckGo, IAsk AI & Monica Search MCP - A Model Context Protocol server for web search

Usage:
  npx -y @oevortex/ddg_search@latest [options]

Options:
  -h, --help     Show this help message
  -v, --version  Show version information

This MCP server provides the following tools:
  - web-search: Search the web using DuckDuckGo
  - iask-search: Search using IAsk AI for AI-generated responses
  - monica-search: Search using Monica AI for AI-generated responses

Created by @OEvortex
Subscribe to youtube.com/@OEvortex for more tools and tutorials!

For more information, visit: https://github.com/OEvortex/ddg_search
  `);
  process.exit(0);
}

if (versionFlag) {
  // Read version from package.json using fs
  import('fs/promises')
    .then(async ({ readFile }) => {
      try {
        const packageJson = JSON.parse(
          await readFile(new URL('../package.json', import.meta.url), 'utf8')
        );
        console.log(`DuckDuckGo & IAsk AI Search MCP v${packageJson.version}\nCreated by @OEvortex - Subscribe to youtube.com/@OEvortex!`);
        process.exit(0);
      } catch (err) {
        console.error('Error reading version information:', err);
        process.exit(1);
      }
    })
    .catch(err => {
      console.error('Error importing fs module:', err);
      process.exit(1);
    });
} else {
  // Start the server
  startServer();
}

```

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

```javascript
import axios from 'axios';
import * as cheerio from 'cheerio';
import https from 'https';
import { getRandomUserAgent } from './user_agents.js';

// Constants
const MAX_CACHE_PAGES = 5;

// Cache results to avoid repeated requests
const resultsCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

// HTTPS agent configuration to handle certificate chain issues
const httpsAgent = new https.Agent({
  rejectUnauthorized: true, // Keep security enabled
  keepAlive: true,
  timeout: 10000,
  // Provide fallback for certificate issues while maintaining security
  secureProtocol: 'TLSv1_2_method'
});

/**
 * Generate a cache key for a search query
 * @param {string} query - The search query
 * @returns {string} The cache key
 */
function getCacheKey(query) {
  return `${query}`;
}

/**
 * Clear old entries from the cache
 */
function clearOldCache() {
  const now = Date.now();
  for (const [key, value] of resultsCache.entries()) {
    if (now - value.timestamp > CACHE_DURATION) {
      resultsCache.delete(key);
    }
  }
}

/**
 * Extract the direct URL from a DuckDuckGo redirect URL
 * @param {string} duckduckgoUrl - The DuckDuckGo URL to extract from
 * @returns {string} The direct URL
 */
function extractDirectUrl(duckduckgoUrl) {
  try {
    // Handle relative URLs from DuckDuckGo
    if (duckduckgoUrl.startsWith('//')) {
      duckduckgoUrl = 'https:' + duckduckgoUrl;
    } else if (duckduckgoUrl.startsWith('/')) {
      duckduckgoUrl = 'https://duckduckgo.com' + duckduckgoUrl;
    }

    const url = new URL(duckduckgoUrl);

    // Extract direct URL from DuckDuckGo redirect
    if (url.hostname === 'duckduckgo.com' && url.pathname === '/l/') {
      const uddg = url.searchParams.get('uddg');
      if (uddg) {
        return decodeURIComponent(uddg);
      }
    }

    // Handle ad redirects
    if (url.hostname === 'duckduckgo.com' && url.pathname === '/y.js') {
      const u3 = url.searchParams.get('u3');
      if (u3) {
        try {
          const decodedU3 = decodeURIComponent(u3);
          const u3Url = new URL(decodedU3);
          const clickUrl = u3Url.searchParams.get('ld');
          if (clickUrl) {
            return decodeURIComponent(clickUrl);
          }
          return decodedU3;
        } catch {
          return duckduckgoUrl;
        }
      }
    }

    return duckduckgoUrl;
  } catch {
    // If URL parsing fails, try to extract URL from a basic string match
    const urlMatch = duckduckgoUrl.match(/https?:\/\/[^\s<>"]+/);
    if (urlMatch) {
      return urlMatch[0];
    }
    return duckduckgoUrl;
  }
}

/**
 * Get a favicon URL for a given website URL
 * @param {string} url - The website URL
 * @returns {string} The favicon URL
 */
function getFaviconUrl(url) {
  try {
    const urlObj = new URL(url);
    return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`;
  } catch {
    return ''; // Return empty string if URL is invalid
  }
}


/**
 * Scrapes search results from DuckDuckGo HTML
 * @param {string} query - The search query
 * @param {number} numResults - Number of results to return (default: 10)
 * @returns {Promise<Array>} - Array of search results
 */
async function searchDuckDuckGo(query, numResults = 10, mode = 'short') {
  try {
    // Clear old cache entries
    clearOldCache();

    // Check cache first
    const cacheKey = getCacheKey(query);
    const cachedResults = resultsCache.get(cacheKey);

    if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
      return cachedResults.results.slice(0, numResults);
    }

    // Get a random user agent
    const userAgent = getRandomUserAgent();

    // Fetch results
    const response = await axios.get(
      `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
      {
        headers: {
          'User-Agent': userAgent
        },
        httpsAgent: httpsAgent
      }
    );

    if (response.status !== 200) {
      throw new Error('Failed to fetch search results');
    }

    const html = response.data;

    // Parse results using cheerio
    const $ = cheerio.load(html);

    const results = [];
    const jinaFetchPromises = [];
    $('.result').each((i, result) => {
      const $result = $(result);
      const titleEl = $result.find('.result__title a');
      const linkEl = $result.find('.result__url');
      const snippetEl = $result.find('.result__snippet');

      const title = titleEl.text()?.trim();
      const rawLink = titleEl.attr('href');
      const description = snippetEl.text()?.trim();
      const displayUrl = linkEl.text()?.trim();

      const directLink = extractDirectUrl(rawLink || '');
      const favicon = getFaviconUrl(directLink);
      const jinaUrl = getJinaAiUrl(directLink);

      if (title && directLink) {
        if (mode === 'detailed') {
          jinaFetchPromises.push(
            axios.get(jinaUrl, {
              headers: {
                'User-Agent': getRandomUserAgent()
              },
              httpsAgent: httpsAgent,
              timeout: 10000
            })
            .then(jinaRes => {
              let jinaContent = '';
              if (jinaRes.status === 200 && typeof jinaRes.data === 'string') {
                const $jina = cheerio.load(jinaRes.data);
                jinaContent = $jina('body').text()
              }
              return {
                title,
                url: directLink,
                snippet: description || '',
                favicon: favicon,
                displayUrl: displayUrl || '',
                Description: jinaContent
              };
            })
            .catch(() => {
              return {
                title,
                url: directLink,
                snippet: description || '',
                favicon: favicon,
                displayUrl: displayUrl || '',
                Description: ''
              };
            })
          );
        } else {
          // short mode: omit Description
          jinaFetchPromises.push(
            Promise.resolve({
              title,
              url: directLink,
              snippet: description || '',
              favicon: favicon,
              displayUrl: displayUrl || ''
            })
          );
        }
      }
    });

    // Wait for all Jina AI fetches to complete
    const jinaResults = await Promise.all(jinaFetchPromises);
    results.push(...jinaResults);

    // Get limited results
    const limitedResults = results.slice(0, numResults);

    // Cache the results
    resultsCache.set(cacheKey, {
      results: limitedResults,
      timestamp: Date.now()
    });

    // If cache is too big, remove oldest entries
    if (resultsCache.size > MAX_CACHE_PAGES) {
      const oldestKey = Array.from(resultsCache.keys())[0];
      resultsCache.delete(oldestKey);
    }

    return limitedResults;
  } catch (error) {
    console.error('Error searching DuckDuckGo:', error.message);
    throw error;
  }
}


export {
  searchDuckDuckGo,
  extractDirectUrl,
  getFaviconUrl
};

/**
 * Generate a Jina AI URL for a given website URL
 * @param {string} url - The website URL
 * @returns {string} The Jina AI URL
 */
function getJinaAiUrl(url) {
  try {
    const urlObj = new URL(url);
    return `https://r.jina.ai/${urlObj.href}`;
  } catch {
    return '';
  }
}

export { getJinaAiUrl };

```

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

```javascript
import axios from 'axios';
import WebSocket from 'ws';
import * as cheerio from 'cheerio';
import TurndownService from 'turndown';
import * as tough from 'tough-cookie';
import { wrapper } from 'axios-cookiejar-support';
import { getRandomUserAgent } from './user_agents.js';

const { CookieJar } = tough;

// Valid modes and detail levels
const VALID_MODES = ['question', 'academic', 'forums', 'wiki', 'thinking'];
const VALID_DETAIL_LEVELS = ['concise', 'detailed', 'comprehensive'];

// Cache results to avoid repeated requests
const resultsCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

const DEFAULT_TIMEOUT = 30000;
const API_ENDPOINT = 'https://iask.ai/';

/**
 * Generate a cache key for a search query
 * @param {string} query - The search query
 * @param {string} mode - The search mode
 * @param {string|null} detailLevel - The detail level
 * @returns {string} The cache key
 */
function getCacheKey(query, mode, detailLevel) {
  return `iask-${mode}-${detailLevel || 'default'}-${query}`;
}

/**
 * Clear old entries from the cache
 */
function clearOldCache() {
  const now = Date.now();
  for (const [key, value] of resultsCache.entries()) {
    if (now - value.timestamp > CACHE_DURATION) {
      resultsCache.delete(key);
    }
  }
}

/**
 * Recursively search for cached HTML content in diff object
 * @param {any} diff - The diff object to search
 * @returns {string|null} The found content or null
 */
function cacheFind(diff) {
  const values = Array.isArray(diff) ? diff : Object.values(diff);
  
  for (const value of values) {
    if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
      const cache = cacheFind(value);
      if (cache) return cache;
    }
    
    if (typeof value === 'string' && /<p>.+?<\/p>/.test(value)) {
      const turndownService = new TurndownService();
      return turndownService.turndown(value).trim();
    }
  }
  
  return null;
}

/**
 * Format HTML content into readable markdown text
 * @param {string} htmlContent - The HTML content to format
 * @returns {string} Formatted text
 */
function formatHtml(htmlContent) {
  if (!htmlContent) return '';
  
  const $ = cheerio.load(htmlContent);
  const outputLines = [];

  $('h1, h2, h3, p, ol, ul, div').each((_, element) => {
    const tagName = element.tagName.toLowerCase();
    const $el = $(element);

    if (['h1', 'h2', 'h3'].includes(tagName)) {
      outputLines.push(`\n**${$el.text().trim()}**\n`);
    } else if (tagName === 'p') {
      let text = $el.text().trim();
      // Remove IAsk attribution
      text = text.replace(/^According to Ask AI & Question AI www\.iAsk\.ai:\s*/i, '').trim();
      // Remove footnote markers
      text = text.replace(/\[\d+\]\(#fn:\d+ 'see footnote'\)/g, '');
      if (text) outputLines.push(text + '\n');
    } else if (['ol', 'ul'].includes(tagName)) {
      $el.find('li').each((_, li) => {
        outputLines.push('- ' + $(li).text().trim() + '\n');
      });
    } else if (tagName === 'div' && $el.hasClass('footnotes')) {
      outputLines.push('\n**Authoritative Sources**\n');
      $el.find('li').each((_, li) => {
        const link = $(li).find('a');
        if (link.length) {
          outputLines.push(`- ${link.text().trim()} (${link.attr('href')})\n`);
        }
      });
    }
  });

  return outputLines.join('');
}

/**
 * Search using IAsk AI via WebSocket (Phoenix LiveView)
 * @param {string} prompt - The search query or prompt
 * @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
 * @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
 * @returns {Promise<string>} The search results
 */
async function searchIAsk(prompt, mode = 'thinking', detailLevel = null) {
  // Validate mode
  if (!VALID_MODES.includes(mode)) {
    throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
  }

  // Validate detail level
  if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
    throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
  }

  // Clear old cache entries
  clearOldCache();

  const cacheKey = getCacheKey(prompt, mode, detailLevel);
  const cachedResults = resultsCache.get(cacheKey);

  if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
    return cachedResults.results;
  }

  // Build URL parameters
  const params = new URLSearchParams({ mode, q: prompt });
  if (detailLevel) {
    params.append('options[detail_level]', detailLevel);
  }

  // Create a cookie jar for session management
  const jar = new CookieJar();
  const client = wrapper(axios.create({ jar }));

  // Get initial page and extract tokens
  const response = await client.get(API_ENDPOINT, {
    params: Object.fromEntries(params),
    timeout: DEFAULT_TIMEOUT,
    headers: {
      'User-Agent': getRandomUserAgent()
    }
  });

  const $ = cheerio.load(response.data);
  
  const phxNode = $('[id^="phx-"]').first();
  const csrfToken = $('[name="csrf-token"]').attr('content');
  const phxId = phxNode.attr('id');
  const phxSession = phxNode.attr('data-phx-session');

  if (!phxId || !csrfToken) {
    throw new Error('Failed to extract required tokens from page');
  }

  // Get the actual response URL (after any redirects)
  const responseUrl = response.request.res?.responseUrl || response.config.url;
  
  // Get cookies from the jar for WebSocket connection
  const cookies = await jar.getCookies(API_ENDPOINT);
  const cookieString = cookies.map(c => `${c.key}=${c.value}`).join('; ');
  
  // Build WebSocket URL
  const wsParams = new URLSearchParams({
    '_csrf_token': csrfToken,
    'vsn': '2.0.0'
  });
  const wsUrl = `wss://iask.ai/live/websocket?${wsParams.toString()}`;

  return new Promise((resolve, reject) => {
    const ws = new WebSocket(wsUrl, {
      headers: {
        'Cookie': cookieString,
        'User-Agent': getRandomUserAgent(),
        'Origin': 'https://iask.ai'
      }
    });
    
    let buffer = '';
    let timeoutId;

    ws.on('open', () => {
      // Send phx_join message
      ws.send(JSON.stringify([
        null,
        null,
        `lv:${phxId}`,
        'phx_join',
        {
          params: { _csrf_token: csrfToken },
          url: responseUrl,
          session: phxSession
        }
      ]));
    });

    ws.on('message', (data) => {
      try {
        const msg = JSON.parse(data.toString());
        if (!msg) return;

        const diff = msg[4];
        if (!diff) return;

        let chunk = null;

        try {
          // Try to get chunk from diff.e[0][1].data
          // Use non-optional chaining to trigger exception if path doesn't exist
          if (diff.e) {
            chunk = diff.e[0][1].data;
            
            if (chunk) {
              let formatted;
              if (/<[^>]+>/.test(chunk)) {
                formatted = formatHtml(chunk);
              } else {
                formatted = chunk.replace(/<br\/>/g, '\n');
              }
              
              buffer += formatted;
            }
          } else {
            throw new Error('No diff.e');
          }
        } catch {
          // Fallback to cacheFind
          const cache = cacheFind(diff);
          if (cache) {
            let formatted;
            if (/<[^>]+>/.test(cache)) {
              formatted = formatHtml(cache);
            } else {
              formatted = cache;
            }
            buffer += formatted;
            // Close after cache find
            ws.close();
            return;
          }
        }
      } catch (err) {
        reject(new Error(`IAsk API error: ${err.message}`));
        ws.close();
      }
    });

    ws.on('close', () => {
      clearTimeout(timeoutId);
      
      // Cache the result
      if (buffer) {
        resultsCache.set(cacheKey, {
          results: buffer,
          timestamp: Date.now()
        });
      }
      
      resolve(buffer || 'No results found.');
    });

    ws.on('error', (err) => {
      clearTimeout(timeoutId);
      reject(new Error(`WebSocket error: ${err.message}`));
    });

    timeoutId = setTimeout(() => {
      ws.close();
    }, DEFAULT_TIMEOUT);
  });
}

export { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS };

```