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