@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 +4 -1
- package/README.md +17 -5
- package/package.json +1 -1
- package/src/tools/searchTool.js +16 -21
- package/src/utils/search.js +80 -23
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
|
+
## [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
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
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
package/src/tools/searchTool.js
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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: '
|
|
26
|
-
default:
|
|
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,
|
|
42
|
-
console.log(`Searching for: ${query} (
|
|
43
|
-
|
|
44
|
-
const results = await searchDuckDuckGo(query,
|
|
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:
|
|
51
|
+
text: JSON.stringify(results)
|
|
57
52
|
}
|
|
58
53
|
]
|
|
59
54
|
};
|
package/src/utils/search.js
CHANGED
|
@@ -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
|
|
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
|
|
46
|
-
return `${query}
|
|
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,
|
|
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
|
|
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)}
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
//
|
|
201
|
-
const
|
|
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:
|
|
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
|
|
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 };
|