@oevortex/ddg_search 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [1.1.5] - 2025-11-30
5
+ ## [1.1.7] - 2025-11-30
6
6
  ### Changed
7
7
  - Replaced Felo AI tool with IAsk AI tool for advanced AI-powered search
8
+ - Added new dependencies: `turndown` for HTML to Markdown conversion, `ws` for WebSocket support
9
+ - Updated README to reflect changes and new tool usage
10
+ - Added new modes: 'short', 'detailed' in web search tool
8
11
  - Added `src/utils/search_iask.js` implementing IAsk API client
9
12
  - Added `src/tools/iaskTool.js` tool definition and handler
10
13
  - Updated `src/index.ts` to use IAsk tool instead of Felo
package/README.md CHANGED
@@ -1,12 +1,22 @@
1
1
  <div align="center">
2
- <img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
3
- <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
4
- <img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
5
- <h1>DuckDuckGo & IAsk AI Search MCP 🔍🧠</h1>
6
- <p>A blazing-fast, privacy-friendly Model Context Protocol (MCP) server for web search and AI-powered responses using DuckDuckGo and IAsk AI.</p>
2
+ <a href="https://www.npmjs.com/package/@oevortex/ddg_search">
3
+ <img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
4
+ </a>
5
+ <a href="https://github.com/OEvortex/ddg_search/blob/main/LICENSE">
6
+ <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
7
+ </a>
8
+ <a href="https://youtube.com/@OEvortex">
9
+ <img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
10
+ </a>
11
+ <h1>DuckDuckGo & IAsk AI Search MCP <span style="font-size:2.2rem;">🔍🧠</span></h1>
12
+ <p style="font-size:1.15rem; max-width:600px; margin:0 auto;">
13
+ <strong>Lightning-fast, privacy-first Model Context Protocol (MCP) server for web search and AI-powered answers.<br>
14
+ Powered by DuckDuckGo and IAsk AI.</strong>
15
+ </p>
7
16
  <a href="https://glama.ai/mcp/servers/@OEvortex/ddg_search">
8
17
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@OEvortex/ddg_search/badge" alt="DuckDuckGo Search MCP server" />
9
18
  </a>
19
+ <br>
10
20
  <a href="https://youtube.com/@OEvortex"><strong>Subscribe for updates & tutorials</strong></a>
11
21
  </div>
12
22
 
@@ -27,6 +37,7 @@
27
37
  <div><b>🆓 No API keys required</b> - works out of the box</div>
28
38
  </div>
29
39
 
40
+
30
41
  > [!IMPORTANT]
31
42
  > Unlike many search tools, this package performs actual web scraping rather than using limited APIs, giving you more comprehensive results.
32
43
 
@@ -42,6 +53,7 @@ npx -y @oevortex/ddg_search@latest
42
53
  ```
43
54
  </div>
44
55
 
56
+
45
57
  > [!TIP]
46
58
  > This will download and run the latest version of the MCP server directly without installation – perfect for quick use with AI assistants.
47
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oevortex/ddg_search",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "A Model Context Protocol server for web search using DuckDuckGo and IAsk AI",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.ts",
@@ -6,26 +6,26 @@ import { searchDuckDuckGo } from '../utils/search.js';
6
6
  export const searchToolDefinition = {
7
7
  name: 'web-search',
8
8
  title: 'Web Search',
9
- description: 'Search the web using DuckDuckGo and return comprehensive results with titles, URLs, and snippets',
9
+ description: 'Perform a web search using DuckDuckGo and receive detailed results including titles, URLs, and summaries.',
10
10
  inputSchema: {
11
11
  type: 'object',
12
12
  properties: {
13
13
  query: {
14
14
  type: 'string',
15
- description: 'The search query to find relevant web pages'
16
- },
17
- page: {
18
- type: 'integer',
19
- description: 'Page number for pagination (default: 1)',
20
- default: 1,
21
- minimum: 1
15
+ description: 'Enter your search query to find the most relevant web pages.'
22
16
  },
23
17
  numResults: {
24
18
  type: 'integer',
25
- description: 'Number of results to return per page (default: 10, max: 20)',
26
- default: 10,
19
+ description: 'Specify how many results to display (default: 3, maximum: 20).',
20
+ default: 3,
27
21
  minimum: 1,
28
22
  maximum: 20
23
+ },
24
+ mode: {
25
+ type: 'string',
26
+ description: "Choose 'short' for basic results (no Description) or 'detailed' for full results (includes Description).",
27
+ enum: ['short', 'detailed'],
28
+ default: 'short'
29
29
  }
30
30
  },
31
31
  required: ['query']
@@ -38,22 +38,17 @@ export const searchToolDefinition = {
38
38
  * @returns {Promise<Object>} - The tool result
39
39
  */
40
40
  export async function searchToolHandler(params) {
41
- const { query, page = 1, numResults = 10 } = params;
42
- console.log(`Searching for: ${query} (page ${page}, ${numResults} results)`);
43
-
44
- const results = await searchDuckDuckGo(query, page, numResults);
41
+ const { query, numResults = 3, mode = 'short' } = params;
42
+ console.log(`Searching for: ${query} (${numResults} results, mode: ${mode})`);
43
+
44
+ const results = await searchDuckDuckGo(query, numResults, mode);
45
45
  console.log(`Found ${results.length} results`);
46
-
47
- // Format the results for display
48
- const formattedResults = results.map((result, index) =>
49
- `${index + 1}. [${result.title}](${result.url})\n ${result.snippet}`
50
- ).join('\n\n');
51
-
46
+
52
47
  return {
53
48
  content: [
54
49
  {
55
50
  type: 'text',
56
- text: formattedResults || 'No results found.'
51
+ text: JSON.stringify(results)
57
52
  }
58
53
  ]
59
54
  };
@@ -3,7 +3,6 @@ import * as cheerio from 'cheerio';
3
3
  import https from 'https';
4
4
 
5
5
  // Constants
6
- const RESULTS_PER_PAGE = 10;
7
6
  const MAX_CACHE_PAGES = 5;
8
7
 
9
8
  // Rotating User Agents
@@ -37,13 +36,12 @@ function getRandomUserAgent() {
37
36
  }
38
37
 
39
38
  /**
40
- * Generate a cache key for a search query and page
39
+ * Generate a cache key for a search query
41
40
  * @param {string} query - The search query
42
- * @param {number} page - The page number
43
41
  * @returns {string} The cache key
44
42
  */
45
- function getCacheKey(query, page) {
46
- return `${query}-${page}`;
43
+ function getCacheKey(query) {
44
+ return `${query}`;
47
45
  }
48
46
 
49
47
  /**
@@ -125,23 +123,20 @@ function getFaviconUrl(url) {
125
123
  }
126
124
  }
127
125
 
126
+
128
127
  /**
129
128
  * Scrapes search results from DuckDuckGo HTML
130
129
  * @param {string} query - The search query
131
- * @param {number} page - The page number (default: 1)
132
130
  * @param {number} numResults - Number of results to return (default: 10)
133
131
  * @returns {Promise<Array>} - Array of search results
134
132
  */
135
- async function searchDuckDuckGo(query, page = 1, numResults = 10) {
133
+ async function searchDuckDuckGo(query, numResults = 10, mode = 'short') {
136
134
  try {
137
135
  // Clear old cache entries
138
136
  clearOldCache();
139
137
 
140
- // Calculate start index for pagination
141
- const startIndex = (page - 1) * RESULTS_PER_PAGE;
142
-
143
138
  // Check cache first
144
- const cacheKey = getCacheKey(query, page);
139
+ const cacheKey = getCacheKey(query);
145
140
  const cachedResults = resultsCache.get(cacheKey);
146
141
 
147
142
  if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
@@ -153,7 +148,7 @@ async function searchDuckDuckGo(query, page = 1, numResults = 10) {
153
148
 
154
149
  // Fetch results
155
150
  const response = await axios.get(
156
- `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}&s=${startIndex}`,
151
+ `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
157
152
  {
158
153
  headers: {
159
154
  'User-Agent': userAgent
@@ -172,6 +167,7 @@ async function searchDuckDuckGo(query, page = 1, numResults = 10) {
172
167
  const $ = cheerio.load(html);
173
168
 
174
169
  const results = [];
170
+ const jinaFetchPromises = [];
175
171
  $('.result').each((i, result) => {
176
172
  const $result = $(result);
177
173
  const titleEl = $result.find('.result__title a');
@@ -185,24 +181,69 @@ async function searchDuckDuckGo(query, page = 1, numResults = 10) {
185
181
 
186
182
  const directLink = extractDirectUrl(rawLink || '');
187
183
  const favicon = getFaviconUrl(directLink);
184
+ const jinaUrl = getJinaAiUrl(directLink);
188
185
 
189
186
  if (title && directLink) {
190
- results.push({
191
- title,
192
- url: directLink,
193
- snippet: description || '',
194
- favicon: favicon,
195
- displayUrl: displayUrl || ''
196
- });
187
+ if (mode === 'detailed') {
188
+ jinaFetchPromises.push(
189
+ axios.get(jinaUrl, {
190
+ headers: {
191
+ 'User-Agent': getRandomUserAgent()
192
+ },
193
+ httpsAgent: httpsAgent,
194
+ timeout: 10000
195
+ })
196
+ .then(jinaRes => {
197
+ let jinaContent = '';
198
+ if (jinaRes.status === 200 && typeof jinaRes.data === 'string') {
199
+ const $jina = cheerio.load(jinaRes.data);
200
+ jinaContent = $jina('body').text()
201
+ }
202
+ return {
203
+ title,
204
+ url: directLink,
205
+ snippet: description || '',
206
+ favicon: favicon,
207
+ displayUrl: displayUrl || '',
208
+ Description: jinaContent
209
+ };
210
+ })
211
+ .catch(() => {
212
+ return {
213
+ title,
214
+ url: directLink,
215
+ snippet: description || '',
216
+ favicon: favicon,
217
+ displayUrl: displayUrl || '',
218
+ Description: ''
219
+ };
220
+ })
221
+ );
222
+ } else {
223
+ // short mode: omit Description
224
+ jinaFetchPromises.push(
225
+ Promise.resolve({
226
+ title,
227
+ url: directLink,
228
+ snippet: description || '',
229
+ favicon: favicon,
230
+ displayUrl: displayUrl || ''
231
+ })
232
+ );
233
+ }
197
234
  }
198
235
  });
199
236
 
200
- // Get paginated results
201
- const paginatedResults = results.slice(0, numResults);
237
+ // Wait for all Jina AI fetches to complete
238
+ const jinaResults = await Promise.all(jinaFetchPromises);
239
+ results.push(...jinaResults);
240
+
241
+ // Get limited results
242
+ const limitedResults = results.slice(0, numResults);
202
243
 
203
244
  // Cache the results
204
245
  resultsCache.set(cacheKey, {
205
- results: paginatedResults,
246
+ results: limitedResults,
206
247
  timestamp: Date.now()
207
248
  });
208
249
 
@@ -212,7 +253,7 @@ async function searchDuckDuckGo(query, page = 1, numResults = 10) {
212
253
  resultsCache.delete(oldestKey);
213
254
  }
214
255
 
215
- return paginatedResults;
256
+ return limitedResults;
216
257
  } catch (error) {
217
258
  console.error('Error searching DuckDuckGo:', error.message);
218
259
  throw error;
@@ -225,3 +266,19 @@ export {
225
266
  extractDirectUrl,
226
267
  getFaviconUrl
227
268
  };
269
+
270
+ /**
271
+ * Generate a Jina AI URL for a given website URL
272
+ * @param {string} url - The website URL
273
+ * @returns {string} The Jina AI URL
274
+ */
275
+ function getJinaAiUrl(url) {
276
+ try {
277
+ const urlObj = new URL(url);
278
+ return `https://r.jina.ai/${urlObj.href}`;
279
+ } catch {
280
+ return '';
281
+ }
282
+ }
283
+
284
+ export { getJinaAiUrl };