#
tokens: 12678/50000 17/17 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .dockerignore
├── .gitignore
├── .npmignore
├── .smithery
│   └── index.cjs
├── bin
│   └── cli.js
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── index.js
│   ├── index.ts
│   ├── tools
│   │   ├── feloTool.js
│   │   ├── fetchUrlTool.js
│   │   ├── metadataTool.js
│   │   └── searchTool.js
│   └── utils
│       ├── search_felo.js
│       └── search.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">
  <img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
  <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
  <img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
  <h1>DuckDuckGo & Felo AI Search MCP 🔍🧠</h1>
  <p>A blazing-fast, privacy-friendly Model Context Protocol (MCP) server for web search and AI-powered responses using DuckDuckGo and Felo AI.</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>
  <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 Felo AI</div>
  <div><b>📄 URL content extraction</b> with smart filtering</div>
  <div><b>📊 URL metadata extraction</b> (title, description, images)</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</b></summary>

```bash
npm install -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
```

</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>🧠 Felo AI Search Tool</b><br/>
    <code>felo-search</code><br/>
    <ul>
      <li><b>query</b> (string, required): The search query or prompt</li>
      <li><b>stream</b> (boolean, optional, default: false): Whether to stream the response</li>
    </ul>
    <i>Example: Search Felo AI for "Explain quantum computing in simple terms"</i>
  </div>
  <div style="margin-bottom: 1.5em;">
    <b>📄 Fetch URL Tool</b><br/>
    <code>fetch-url</code><br/>
    <ul>
      <li><b>url</b> (string, required): The URL to fetch</li>
      <li><b>maxLength</b> (integer, optional, default: 10000): Max content length</li>
      <li><b>extractMainContent</b> (boolean, optional, default: true): Extract main content</li>
      <li><b>includeLinks</b> (boolean, optional, default: true): Include link text</li>
      <li><b>includeImages</b> (boolean, optional, default: true): Include image alt text</li>
      <li><b>excludeTags</b> (array, optional): Tags to exclude</li>
    </ul>
    <i>Example: Fetch the content from "https://example.com"</i>
  </div>
  <div style="margin-bottom: 1.5em;">
    <b>📊 URL Metadata Tool</b><br/>
    <code>url-metadata</code><br/>
    <ul>
      <li><b>url</b> (string, required): The URL to extract metadata from</li>
    </ul>
    <i>Example: Get metadata for "https://example.com"</i>
  </div>
</div>

---

## 📁 Project Structure


```text
bin/              # Command-line interface
src/
  index.js        # Main entry point
  tools/          # Tool definitions and handlers
    searchTool.js
    fetchUrlTool.js
    metadataTool.js
    feloTool.js
  utils/
    search.js     # Search and URL utilities
    search_felo.js # Felo 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"]

```

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

```json
{"name":"@oevortex/ddg_search","version":"1.1.2","description":"A Model Context Protocol server for web search using DuckDuckGo and Felo 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","felo","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","cheerio":"^1.0.0","jsdom":"^26.1.0","smithery":"^0.5.2","uuid":"^9.0.1"},"devDependencies":{"@types/node":"^24.3.0","tsx":"^4.20.4","typescript":"^5.9.2"}}
```

--------------------------------------------------------------------------------
/src/tools/metadataTool.js:
--------------------------------------------------------------------------------

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

/**
 * URL metadata tool definition
 */
export const metadataToolDefinition = {
  name: 'url-metadata',
  title: 'URL Metadata Extractor',
  description: 'Extract metadata from a URL including title, description, Open Graph data, and favicon information',
  inputSchema: {
    type: 'object',
    properties: {
      url: {
        type: 'string',
        description: 'The URL to extract metadata from (must be a valid HTTP/HTTPS URL)'
      }
    },
    required: ['url']
  }
};

/**
 * URL metadata tool handler
 * @param {Object} params - The tool parameters
 * @returns {Promise<Object>} - The tool result
 */
export async function metadataToolHandler(params) {
  const { url } = params;
  console.log(`Extracting metadata from URL: ${url}`);
  
  const metadata = await extractUrlMetadata(url);
  
  // Format the metadata for display
  const formattedMetadata = `
## URL Metadata for ${url}

**Title:** ${metadata.title}

**Description:** ${metadata.description}

**Image:** ${metadata.ogImage || 'None'}

**Favicon:** ${metadata.favicon || 'None'}
  `.trim();
  
  return {
    content: [
      {
        type: 'text',
        text: formattedMetadata
      }
    ]
  };
}

```

--------------------------------------------------------------------------------
/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: 'Search the web using DuckDuckGo and return comprehensive results with titles, URLs, and snippets',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: 'The search query to find relevant web pages'
      },
      page: {
        type: 'integer',
        description: 'Page number for pagination (default: 1)',
        default: 1,
        minimum: 1
      },
      numResults: {
        type: 'integer',
        description: 'Number of results to return per page (default: 10, max: 20)',
        default: 10,
        minimum: 1,
        maximum: 20
      }
    },
    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, page = 1, numResults = 10 } = params;
  console.log(`Searching for: ${query} (page ${page}, ${numResults} results)`);
  
  const results = await searchDuckDuckGo(query, page, numResults);
  console.log(`Found ${results.length} results`);
  
  // Format the results for display
  const formattedResults = results.map((result, index) => 
    `${index + 1}. [${result.title}](${result.url})\n   ${result.snippet}`
  ).join('\n\n');
  
  return {
    content: [
      {
        type: 'text',
        text: formattedResults || 'No results found.'
      }
    ]
  };
}

```

--------------------------------------------------------------------------------
/src/tools/feloTool.js:
--------------------------------------------------------------------------------

```javascript
import { searchFelo } from '../utils/search_felo.js';

/**
 * Felo AI search tool definition
 */
export const feloToolDefinition = {
  name: 'felo-search',
  title: 'Felo AI Advanced Search',
  description: 'Advanced AI-powered web search for technical intelligence. Retrieves up-to-date information including software releases, security advisories, migration guides, benchmarks, developer documentation, and community insights. Supports both standard and streaming responses.',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: 'A detailed search query or prompt describing the technical information needed. Supports natural language and keyword-based queries for precise results.'
      },
      stream: {
        type: 'boolean',
        description: 'Enable streaming mode to receive incremental, real-time search results as they are discovered. Useful for monitoring live updates or large result sets. Default is false (returns full result at once).',
        default: false
      }
    },
    required: ['query']
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: false
  }
};

/**
 * Felo AI search tool handler
 * @param {Object} params - The tool parameters
 * @returns {Promise<Object>} - The tool result
 */
export async function feloToolHandler(params) {
  const { query, stream = false } = params;
  console.log(`Searching Felo AI for: "${query}" (stream: ${stream})`);
  
  try {
    if (stream) {
      // For streaming responses, we need to collect them and then return
      let fullResponse = '';
      const chunks = [];
      
      for await (const chunk of await searchFelo(query, true)) {
        chunks.push(chunk);
        fullResponse += chunk;
      }
      
      // Format the response
      return {
        content: [
          {
            type: 'text',
            text: fullResponse || 'No results found.'
          }
        ]
      };
    } else {
      // For non-streaming responses
      const response = await searchFelo(query, false);
      
      return {
        content: [
          {
            type: 'text',
            text: response || 'No results found.'
          }
        ]
      };
    }
  } catch (error) {
    console.error(`Error in Felo search: ${error.message}`);
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: `Error searching Felo: ${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 { fetchUrlToolDefinition, fetchUrlToolHandler } from './tools/fetchUrlTool.js';
import { metadataToolDefinition, metadataToolHandler } from './tools/metadataTool.js';
import { feloToolDefinition, feloToolHandler } from './tools/feloTool.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,
    fetchUrlToolDefinition,
    metadataToolDefinition,
    feloToolDefinition
  ];
  
  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 'fetch-url':
          return await fetchUrlToolHandler(args);
        
        case 'url-metadata':
          return await metadataToolHandler(args);
        
        case 'felo-search':
          return await feloToolHandler(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/tools/fetchUrlTool.js:
--------------------------------------------------------------------------------

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

/**
 * Fetch URL tool definition
 */
export const fetchUrlToolDefinition = {
  name: 'fetch-url',
  title: 'Fetch URL Content',
  description: 'Fetch and extract the main content from any URL, with customizable extraction options for text, links, and images',
  inputSchema: {
    type: 'object',
    properties: {
      url: {
        type: 'string',
        description: 'The URL to fetch content from (must be a valid HTTP/HTTPS URL)'
      },
      maxLength: {
        type: 'integer',
        description: 'Maximum length of content to return in characters (default: 10000)',
        default: 10000,
        minimum: 1000,
        maximum: 50000
      },
      extractMainContent: {
        type: 'boolean',
        description: 'Whether to attempt to extract main content only, filtering out navigation and ads (default: true)',
        default: true
      },
      includeLinks: {
        type: 'boolean',
        description: 'Whether to include link text in the extracted content (default: true)',
        default: true
      },
      includeImages: {
        type: 'boolean',
        description: 'Whether to include image alt text in the extracted content (default: true)',
        default: true
      },
      excludeTags: {
        type: 'array',
        description: 'HTML tags to exclude from extraction (default: script, style, etc.)',
        items: {
          type: 'string'
        }
      }
    },
    required: ['url']
  }
};

/**
 * Fetch URL tool handler
 * @param {Object} params - The tool parameters
 * @returns {Promise<Object>} - The tool result
 */
export async function fetchUrlToolHandler(params) {
  const {
    url,
    maxLength = 10000,
    extractMainContent = true,
    includeLinks = true,
    includeImages = true,
    excludeTags = ['script', 'style', 'noscript', 'iframe', 'svg', 'nav', 'footer', 'header', 'aside']
  } = params;

  console.log(`Fetching content from URL: ${url} (maxLength: ${maxLength})`);

  try {
    // Fetch content with specified options
    const content = await fetchUrlContent(url, {
      extractMainContent,
      includeLinks,
      includeImages,
      excludeTags
    });

    // Truncate content if it's too long
    const truncatedContent = content.length > maxLength
      ? content.substring(0, maxLength) + '... [Content truncated due to length]'
      : content;

    // Add metadata about the extraction
    const metadata = `
---
Extraction settings:
- URL: ${url}
- Main content extraction: ${extractMainContent ? 'Enabled' : 'Disabled'}
- Links included: ${includeLinks ? 'Yes' : 'No'}
- Images included: ${includeImages ? 'Yes (as alt text)' : 'No'}
- Content length: ${content.length} characters${content.length > maxLength ? ` (truncated to ${maxLength})` : ''}
---
`;

    return {
      content: [
        {
          type: 'text',
          text: truncatedContent + metadata
        }
      ]
    };
  } catch (error) {
    console.error(`Error fetching URL ${url}:`, error);
    return {
      isError: true,
      content: [
        {
          type: 'text',
          text: `Error fetching URL: ${error.message}`
        }
      ]
    };
  }
}

```

--------------------------------------------------------------------------------
/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 { fetchUrlToolDefinition, fetchUrlToolHandler } from './tools/fetchUrlTool.js';
import { metadataToolDefinition, metadataToolHandler } from './tools/metadataTool.js';
import { feloToolDefinition, feloToolHandler } from './tools/feloTool.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,
    fetchUrlToolDefinition,
    metadataToolDefinition,
    feloToolDefinition
  ];
  
  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 'fetch-url':
          return await fetchUrlToolHandler(args);
        
        case 'url-metadata':
          return await metadataToolHandler(args);
        
        case 'felo-search':
          return await feloToolHandler(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();
}

```

--------------------------------------------------------------------------------
/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 { fetchUrlToolDefinition, fetchUrlToolHandler } = await import(`${modulePath}/tools/fetchUrlTool.js`);
    const { metadataToolDefinition, metadataToolHandler } = await import(`${modulePath}/tools/metadataTool.js`);
    const { feloToolDefinition, feloToolHandler } = await import(`${modulePath}/tools/feloTool.js`);    // Create the MCP server
    const server = new Server({
      id: 'ddg-search-mcp',
      name: 'DuckDuckGo & Felo AI Search MCP',
      description: 'A Model Context Protocol server for web search using DuckDuckGo and Felo AI',
      version: '1.1.2'
    }, {
      capabilities: {
        tools: {
          listChanged: true
        }
      }
    });

    // Global variable to track available tools
    let availableTools = [
      searchToolDefinition,
      fetchUrlToolDefinition,
      metadataToolDefinition,
      feloToolDefinition
    ];

    // 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', 'fetch-url', 'url-metadata', 'felo-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 'fetch-url':
            return await fetchUrlToolHandler(args);

          case 'url-metadata':
            return await metadataToolHandler(args);
          
          case 'felo-search':
            return await feloToolHandler(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 & Felo 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 & Felo AI 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 & Felo AI 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
  - fetch-url: Fetch and extract content from a URL
  - url-metadata: Extract metadata from a URL
  - felo-search: Search using Felo 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 & Felo 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_felo.js:
--------------------------------------------------------------------------------

```javascript
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import https from 'https';

// Rotating User Agents
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'
];

// 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: 30000,
  // Provide fallback for certificate issues while maintaining security
  secureProtocol: 'TLSv1_2_method'
});

// Create a persistent axios instance to maintain session state
const feloSession = axios.create({
  timeout: 30000,
  httpsAgent: httpsAgent,
  headers: {
    'accept': '*/*',
    'accept-encoding': 'gzip, deflate, br, zstd',
    'accept-language': 'en-US,en;q=0.9,en-IN;q=0.8',
    'content-type': 'application/json',
    'dnt': '1',
    'origin': 'https://felo.ai',
    'referer': 'https://felo.ai/',
    'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'empty',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'same-site',
    'user-agent': getRandomUserAgent()
  }
});

/**
 * Response class for Felo API responses
 */
class Response {
  /**
   * Create a new Response
   * @param {string} text - The text content of the response
   */
  constructor(text) {
    this.text = text;
  }

  /**
   * String representation of the response
   * @returns {string} The text content
   */
  toString() {
    return this.text;
  }
}

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

/**
 * Generate a cache key for a search query
 * @param {string} query - The search query
 * @returns {string} The cache key
 */
function getCacheKey(query) {
  return `felo-${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);
    }
  }
}

/**
 * Search using the Felo AI API
 * @param {string} prompt - The search query or prompt
 * @param {boolean} stream - If true, yields response chunks as they arrive
 * @param {boolean} raw - If true, returns raw response dictionaries
 * @returns {Promise<string|AsyncGenerator<string>>} The search results
 */
async function searchFelo(prompt, stream = false, raw = false) {
  // Clear old cache entries
  clearOldCache();

  // Check cache first if not streaming
  if (!stream) {
    const cacheKey = getCacheKey(prompt);
    const cachedResults = resultsCache.get(cacheKey);

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

  // Create payload for Felo API with proper structure from reference
  const payload = {
    query: prompt,
    search_uuid: uuidv4().replace(/-/g, ''), // Remove dashes like in reference
    lang: "",
    agent_lang: "en",
    search_options: {
      langcode: "en-US",
      search_image: true,
      search_video: true
    },
    search_video: true,
    model: "",
    contexts_from: "google",
    auto_routing: true
  };

  // Update user agent for this request
  feloSession.defaults.headers['user-agent'] = getRandomUserAgent();

  // Define the streaming function
  async function* streamFunction() {
    try {
      const response = await feloSession.post('https://api.felo.ai/search/threads', payload, {
        responseType: 'stream'
      });

      // Check for HTTP errors
      if (response.status !== 200) {
        throw new Error(`Failed to generate response - (${response.status}, ${response.statusText}) - ${response.data}`);
      }

      let streamingText = '';
      let buffer = '';

      // Process the stream as it comes in
      for await (const chunk of response.data) {
        buffer += chunk.toString();
        
        const lines = buffer.split('\n');
        buffer = lines.pop() || ''; // Keep the last (potentially incomplete) line in the buffer
        
        for (const line of lines) {
          if (line.startsWith('data:')) {
            try {
              const dataStr = line.substring(5).trim();
              if (dataStr) {
                const data = JSON.parse(dataStr);
                if (data.type === 'answer' && 'text' in data.data) {
                  const newText = data.data.text;
                  if (newText.length > streamingText.length) {
                    const delta = newText.substring(streamingText.length);
                    streamingText = newText;
                    
                    if (raw) {
                      yield { text: delta };
                    } else {
                      yield new Response(delta).toString();
                    }
                  }
                }
              }
            } catch (error) {
              // Ignore JSON parse errors and continue
              console.debug('JSON parse error:', error.message);
            }
          }
        }
      }
      
      // Cache the complete response
      if (streamingText) {
        resultsCache.set(getCacheKey(prompt), {
          results: streamingText,
          timestamp: Date.now()
        });
      }
      
    } catch (error) {
      console.error('Error searching Felo:', error.message);
      
      // Handle specific API errors
      if (error.response) {
        const status = error.response.status;
        const statusText = error.response.statusText;
        const data = error.response.data;
        throw new Error(`Felo API error: ${status} ${statusText} - ${data}`);
      }
      
      throw new Error(`Failed to search Felo: ${error.message}`);
    }
  }

  // If streaming is requested, return the generator
  if (stream) {
    return streamFunction();
  }
  
  // For non-streaming, collect all chunks and return as a single string
  let fullResponse = '';
  
  try {
    for await (const chunk of streamFunction()) {
      if (raw) {
        fullResponse += chunk.text;
      } else {
        fullResponse += chunk;
      }
    }
    
    return fullResponse;
  } catch (error) {
    console.error('Error in non-streaming Felo search:', error.message);
    throw error;
  }
}

export { searchFelo };

```

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

```javascript
import axios from 'axios';
import * as cheerio from 'cheerio';
import https from 'https';

// Constants
const RESULTS_PER_PAGE = 10;
const MAX_CACHE_PAGES = 5;

// Rotating User Agents
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'
];

// 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'
});

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

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

/**
 * 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} page - The page number (default: 1)
 * @param {number} numResults - Number of results to return (default: 10)
 * @returns {Promise<Array>} - Array of search results
 */
async function searchDuckDuckGo(query, page = 1, numResults = 10) {
  try {
    // Clear old cache entries
    clearOldCache();

    // Calculate start index for pagination
    const startIndex = (page - 1) * RESULTS_PER_PAGE;

    // Check cache first
    const cacheKey = getCacheKey(query, page);
    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)}&s=${startIndex}`,
      {
        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 = [];
    $('.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);

      if (title && directLink) {
        results.push({
          title,
          url: directLink,
          snippet: description || '',
          favicon: favicon,
          displayUrl: displayUrl || ''
        });
      }
    });

    // Get paginated results
    const paginatedResults = results.slice(0, numResults);

    // Cache the results
    resultsCache.set(cacheKey, {
      results: paginatedResults,
      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 paginatedResults;
  } catch (error) {
    console.error('Error searching DuckDuckGo:', error.message);
    throw error;
  }
}

/**
 * Fetches the content of a URL and returns it as text
 * @param {string} url - The URL to fetch
 * @param {Object} options - Options for content extraction
 * @param {boolean} options.extractMainContent - Whether to attempt to extract main content (default: true)
 * @param {boolean} options.includeLinks - Whether to include link text (default: true)
 * @param {boolean} options.includeImages - Whether to include image alt text (default: true)
 * @param {string[]} options.excludeTags - Tags to exclude from extraction
 * @returns {Promise<string>} - The content of the URL
 */
async function fetchUrlContent(url, options = {}) {
  try {
    // Default options
    const {
      extractMainContent = true,
      includeLinks = true,
      includeImages = true,
      excludeTags = ['script', 'style', 'noscript', 'iframe', 'svg', 'nav', 'footer', 'header', 'aside']
    } = options;

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

    const response = await axios.get(url, {
      headers: {
        'User-Agent': userAgent
      },
      timeout: 10000, // 10 second timeout
      httpsAgent: httpsAgent
    });

    if (response.status !== 200) {
      throw new Error(`Failed to fetch URL: ${url}`);
    }

    // If the content is HTML, extract the text content
    const contentType = response.headers['content-type'] || '';
    if (contentType.includes('text/html')) {
      const $ = cheerio.load(response.data);

      // Remove unwanted elements
      excludeTags.forEach(tag => {
        $(tag).remove();
      });

      // Remove ads and other common unwanted elements
      const unwantedSelectors = [
        '[id*="ad"]', '[class*="ad"]', '[id*="banner"]', '[class*="banner"]',
        '[id*="popup"]', '[class*="popup"]', '[class*="cookie"]',
        '[id*="cookie"]', '[class*="newsletter"]', '[id*="newsletter"]',
        '[class*="social"]', '[id*="social"]', '[class*="share"]', '[id*="share"]'
      ];

      unwantedSelectors.forEach(selector => {
        try {
          $(selector).remove();
        } catch (e) {
          // Ignore invalid selectors
        }
      });

      // Handle links and images
      if (!includeLinks) {
        $('a').each((i, link) => {
          $(link).replaceWith($(link).text());
        });
      }

      if (!includeImages) {
        $('img').remove();
      } else {
        // Replace images with their alt text
        $('img').each((i, img) => {
          const alt = $(img).attr('alt');
          if (alt) {
            $(img).replaceWith(`[Image: ${alt}]`);
          } else {
            $(img).remove();
          }
        });
      }

      // Try to extract main content if requested
      if (extractMainContent) {
        // Common content selectors in order of priority
        const contentSelectors = [
          'article', 'main', '[role="main"]', '.post-content', '.article-content',
          '.content', '#content', '.post', '.article', '.entry-content',
          '.page-content', '.post-body', '.post-text', '.story-body'
        ];

        for (const selector of contentSelectors) {
          const mainContent = $(selector).first();
          if (mainContent.length > 0) {
            // Clean up the content
            return cleanText(mainContent.text());
          }
        }
      }

      // If no main content found or not requested, use the body
      return cleanText($('body').text());
    }

    // For non-HTML content, return as is
    return response.data.toString();
  } catch (error) {
    console.error('Error fetching URL content:', error.message);
    throw error;
  }
}

/**
 * Cleans up text by removing excessive whitespace and normalizing line breaks
 * @param {string} text - The text to clean
 * @returns {string} - The cleaned text
 */
function cleanText(text) {
  return text
    .replace(/\s+/g, ' ')  // Replace multiple whitespace with single space
    .replace(/\n\s*\n/g, '\n\n')  // Normalize multiple line breaks
    .replace(/^\s+|\s+$/g, '')  // Trim start and end
    .trim();
}

/**
 * Extracts metadata from a URL (title, description, etc.)
 * @param {string} url - The URL to extract metadata from
 * @returns {Promise<Object>} - The metadata
 */
async function extractUrlMetadata(url) {
  try {
    // Get a random user agent
    const userAgent = getRandomUserAgent();

    const response = await axios.get(url, {
      headers: {
        'User-Agent': userAgent
      },
      httpsAgent: httpsAgent
    });

    if (response.status !== 200) {
      throw new Error(`Failed to fetch URL: ${url}`);
    }

    const $ = cheerio.load(response.data);

    // Extract metadata
    const title = $('title').text() || '';
    const description = $('meta[name="description"]').attr('content') ||
                       $('meta[property="og:description"]').attr('content') || '';
    const ogImage = $('meta[property="og:image"]').attr('content') || '';
    const favicon = $('link[rel="icon"]').attr('href') ||
                  $('link[rel="shortcut icon"]').attr('href') || '';

    // Resolve relative URLs
    const resolvedFavicon = favicon ? new URL(favicon, url).href : getFaviconUrl(url);
    const resolvedOgImage = ogImage ? new URL(ogImage, url).href : '';

    return {
      title,
      description,
      ogImage: resolvedOgImage,
      favicon: resolvedFavicon,
      url
    };
  } catch (error) {
    console.error('Error extracting URL metadata:', error.message);
    throw error;
  }
}

export {
  searchDuckDuckGo,
  fetchUrlContent,
  extractUrlMetadata,
  extractDirectUrl,
  getFaviconUrl
};

```