mcp-searxng 1.5.0 → 1.7.0
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/README.md +17 -1
- package/dist/cache.d.ts +5 -1
- package/dist/cache.js +44 -6
- package/dist/index.js +1 -1
- package/dist/instance-info.d.ts +2 -0
- package/dist/instance-info.js +79 -9
- package/dist/resources.js +4 -3
- package/dist/search.d.ts +1 -1
- package/dist/search.js +289 -57
- package/dist/types.d.ts +3 -1
- package/dist/types.js +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -91,7 +91,8 @@ AI Assistant (e.g. Claude)
|
|
|
91
91
|
- `safesearch` (number, optional): Safe search filter level (0: None, 1: Moderate, 2: Strict) (default: instance setting)
|
|
92
92
|
- `min_score` (number, optional): Minimum relevance score from 0.0 to 1.0. Results below this score are filtered out.
|
|
93
93
|
- `num_results` (number, optional): Maximum number of results to return, from 1 to 20. `SEARXNG_MAX_RESULTS` applies as an operator ceiling.
|
|
94
|
-
- `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`).
|
|
94
|
+
- `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`). When live `/config` is available, values are trimmed and normalized case-insensitively to the instance's canonical category names; unknown values are rejected with available categories listed. If `/config` is unavailable, values are forwarded as-is with a warning. Default: SearXNG instance default.
|
|
95
|
+
- `engines` (string, optional): Comma-separated SearXNG engine names (e.g. `"google,bing,ddg"`, `"semantic scholar"`). When live `/config` is available, values are trimmed and normalized case-insensitively to canonical engine names, including engines disabled by default; unknown values are rejected with available engines listed. If `/config` is unavailable, values are forwarded as-is with a warning.
|
|
95
96
|
- `response_format` (string, optional): Response format, either `"text"` for formatted agent-readable output or `"json"` for raw SearXNG JSON with filtered/sliced `results`. (default: `"text"`)
|
|
96
97
|
|
|
97
98
|
- **searxng_search_suggestions**
|
|
@@ -272,6 +273,21 @@ You should receive a JSON response. If not, confirm the file is correctly mounte
|
|
|
272
273
|
|
|
273
274
|
See also: [SearXNG settings docs](https://docs.searxng.org/admin/settings/settings.html) · [discussion](https://github.com/searxng/searxng/discussions/1789)
|
|
274
275
|
|
|
276
|
+
### Can't enable JSON? (HTML fallback)
|
|
277
|
+
|
|
278
|
+
If you must use a public instance you don't control and it rejects `format=json` (the 403 above), set the opt-in flag instead of editing the server:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
"SEARXNG_HTML_FALLBACK": "true"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A search that gets a `403`/`404` or a non-JSON response is then retried automatically **without** `format=json` and parsed from the regular HTML results page.
|
|
285
|
+
|
|
286
|
+
- **On success:** you get normal results (title, URL, snippet). They are marked `sourceFormat: "html"` in JSON mode, and text mode adds the line *"Note: Results parsed from SearXNG HTML fallback; metadata is limited."* Relevance scores and engine names are not available from HTML.
|
|
287
|
+
- **On failure:** parsing is best-effort and varies by the instance's theme/version, so some results may be missed or sparse. If the HTML page itself also fails — still blocked, rate-limited (`429`), auth (`401`), or `5xx` — the **original error is surfaced unchanged**. The fallback only triggers on `403`/`404`/non-JSON, never on auth or network errors.
|
|
288
|
+
|
|
289
|
+
Enabling JSON on an instance you control (above) remains the recommended setup — the fallback is a compatibility aid, not a replacement.
|
|
290
|
+
|
|
275
291
|
## Contributing
|
|
276
292
|
|
|
277
293
|
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
package/dist/cache.d.ts
CHANGED
|
@@ -2,14 +2,17 @@ interface CacheEntry {
|
|
|
2
2
|
htmlContent: string;
|
|
3
3
|
markdownContent: string;
|
|
4
4
|
timestamp: number;
|
|
5
|
+
hitCount: number;
|
|
5
6
|
}
|
|
6
7
|
declare class SimpleCache {
|
|
7
8
|
private cache;
|
|
8
9
|
private readonly ttlMs;
|
|
10
|
+
private readonly maxEntries;
|
|
9
11
|
private cleanupInterval;
|
|
10
|
-
constructor(ttlMs?: number, cleanupIntervalMs?: number);
|
|
12
|
+
constructor(ttlMs?: number, maxEntries?: number, cleanupIntervalMs?: number);
|
|
11
13
|
private startCleanup;
|
|
12
14
|
private cleanupExpired;
|
|
15
|
+
private evictIfNeeded;
|
|
13
16
|
get(url: string): CacheEntry | null;
|
|
14
17
|
set(url: string, htmlContent: string, markdownContent: string): void;
|
|
15
18
|
clear(): void;
|
|
@@ -19,6 +22,7 @@ declare class SimpleCache {
|
|
|
19
22
|
entries: Array<{
|
|
20
23
|
url: string;
|
|
21
24
|
age: number;
|
|
25
|
+
hitCount: number;
|
|
22
26
|
}>;
|
|
23
27
|
};
|
|
24
28
|
}
|
package/dist/cache.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
const DEFAULT_CACHE_TTL_MS = 86400000;
|
|
2
|
+
const DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
3
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 60000;
|
|
4
|
+
function parsePositiveInteger(value, fallback) {
|
|
5
|
+
if (value === undefined || value.trim() === "") {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
const parsed = parseInt(value, 10);
|
|
9
|
+
return Number.isNaN(parsed) || parsed <= 0 ? fallback : parsed;
|
|
10
|
+
}
|
|
11
|
+
function normalizePositiveInteger(value, fallback) {
|
|
12
|
+
return !Number.isFinite(value) || !Number.isInteger(value) || value <= 0 ? fallback : value;
|
|
13
|
+
}
|
|
1
14
|
class SimpleCache {
|
|
2
15
|
cache = new Map();
|
|
3
16
|
ttlMs;
|
|
17
|
+
maxEntries;
|
|
4
18
|
cleanupInterval = null;
|
|
5
|
-
constructor(ttlMs =
|
|
6
|
-
this.ttlMs = ttlMs;
|
|
7
|
-
this.
|
|
19
|
+
constructor(ttlMs = parsePositiveInteger(process.env.CACHE_TTL_MS, DEFAULT_CACHE_TTL_MS), maxEntries = parsePositiveInteger(process.env.CACHE_MAX_ENTRIES, DEFAULT_CACHE_MAX_ENTRIES), cleanupIntervalMs = DEFAULT_CLEANUP_INTERVAL_MS) {
|
|
20
|
+
this.ttlMs = normalizePositiveInteger(ttlMs, DEFAULT_CACHE_TTL_MS);
|
|
21
|
+
this.maxEntries = normalizePositiveInteger(maxEntries, DEFAULT_CACHE_MAX_ENTRIES);
|
|
22
|
+
this.startCleanup(normalizePositiveInteger(cleanupIntervalMs, DEFAULT_CLEANUP_INTERVAL_MS));
|
|
8
23
|
}
|
|
9
24
|
startCleanup(cleanupIntervalMs) {
|
|
10
|
-
// Clean up expired entries every cleanupIntervalMs milliseconds (default
|
|
25
|
+
// Clean up expired entries every cleanupIntervalMs milliseconds (default 60s)
|
|
11
26
|
this.cleanupInterval = setInterval(() => {
|
|
12
27
|
this.cleanupExpired();
|
|
13
28
|
}, cleanupIntervalMs);
|
|
@@ -21,6 +36,25 @@ class SimpleCache {
|
|
|
21
36
|
}
|
|
22
37
|
}
|
|
23
38
|
}
|
|
39
|
+
evictIfNeeded() {
|
|
40
|
+
this.cleanupExpired();
|
|
41
|
+
while (this.cache.size > this.maxEntries) {
|
|
42
|
+
let evictionKey = null;
|
|
43
|
+
let evictionEntry = null;
|
|
44
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
45
|
+
if (evictionEntry === null ||
|
|
46
|
+
entry.hitCount < evictionEntry.hitCount ||
|
|
47
|
+
(entry.hitCount === evictionEntry.hitCount && entry.timestamp < evictionEntry.timestamp)) {
|
|
48
|
+
evictionKey = key;
|
|
49
|
+
evictionEntry = entry;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (evictionKey === null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.cache.delete(evictionKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
24
58
|
get(url) {
|
|
25
59
|
const entry = this.cache.get(url);
|
|
26
60
|
if (!entry) {
|
|
@@ -31,14 +65,17 @@ class SimpleCache {
|
|
|
31
65
|
this.cache.delete(url);
|
|
32
66
|
return null;
|
|
33
67
|
}
|
|
68
|
+
entry.hitCount++;
|
|
34
69
|
return entry;
|
|
35
70
|
}
|
|
36
71
|
set(url, htmlContent, markdownContent) {
|
|
37
72
|
this.cache.set(url, {
|
|
38
73
|
htmlContent,
|
|
39
74
|
markdownContent,
|
|
40
|
-
timestamp: Date.now()
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
hitCount: 0
|
|
41
77
|
});
|
|
78
|
+
this.evictIfNeeded();
|
|
42
79
|
}
|
|
43
80
|
clear() {
|
|
44
81
|
this.cache.clear();
|
|
@@ -55,7 +92,8 @@ class SimpleCache {
|
|
|
55
92
|
const now = Date.now();
|
|
56
93
|
const entries = Array.from(this.cache.entries()).map(([url, entry]) => ({
|
|
57
94
|
url,
|
|
58
|
-
age: now - entry.timestamp
|
|
95
|
+
age: now - entry.timestamp,
|
|
96
|
+
hitCount: entry.hitCount
|
|
59
97
|
}));
|
|
60
98
|
return {
|
|
61
99
|
size: this.cache.size,
|
package/dist/index.js
CHANGED
|
@@ -104,7 +104,7 @@ export function createMcpServer() {
|
|
|
104
104
|
if (!isSearXNGWebSearchArgs(args)) {
|
|
105
105
|
throw new Error("Invalid arguments for web search");
|
|
106
106
|
}
|
|
107
|
-
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch, args.min_score, args.num_results, args.categories, args.response_format);
|
|
107
|
+
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch, args.min_score, args.num_results, args.categories, args.engines, args.response_format);
|
|
108
108
|
return {
|
|
109
109
|
content: [
|
|
110
110
|
{
|
package/dist/instance-info.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
export declare function clearInstanceInfoCacheForTests(): void;
|
|
3
|
+
export declare function getKnownEngines(mcpServer: McpServer, refresh?: boolean): Promise<Set<string> | null>;
|
|
4
|
+
export declare function getKnownCategories(mcpServer: McpServer, refresh?: boolean): Promise<Set<string> | null>;
|
|
3
5
|
export declare function fetchInstanceInfo(mcpServer: McpServer, includeEngines?: boolean, includeDisabled?: boolean, category?: string, refresh?: boolean): Promise<string>;
|
package/dist/instance-info.js
CHANGED
|
@@ -9,11 +9,39 @@ function unavailable(message, status) {
|
|
|
9
9
|
...(status !== undefined ? { status } : {}),
|
|
10
10
|
}, null, 2);
|
|
11
11
|
}
|
|
12
|
+
function categoryNamesFromEngines(config) {
|
|
13
|
+
const names = new Set();
|
|
14
|
+
if (Array.isArray(config.engines)) {
|
|
15
|
+
for (const engine of config.engines) {
|
|
16
|
+
for (const category of engineCategories(engine)) {
|
|
17
|
+
if (typeof category === "string" && category.trim() !== "") {
|
|
18
|
+
names.add(category);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...names];
|
|
24
|
+
}
|
|
12
25
|
function namesFromCategories(config) {
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
const names = new Set();
|
|
27
|
+
if (Array.isArray(config.categories)) {
|
|
28
|
+
for (const category of config.categories) {
|
|
29
|
+
if (typeof category === "string" && category.trim() !== "") {
|
|
30
|
+
names.add(category);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (config.categories && typeof config.categories === "object") {
|
|
35
|
+
for (const category of Object.keys(config.categories)) {
|
|
36
|
+
if (category.trim() !== "") {
|
|
37
|
+
names.add(category);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
15
40
|
}
|
|
16
|
-
|
|
41
|
+
for (const category of categoryNamesFromEngines(config)) {
|
|
42
|
+
names.add(category);
|
|
43
|
+
}
|
|
44
|
+
return [...names].sort();
|
|
17
45
|
}
|
|
18
46
|
function engineCategories(engine) {
|
|
19
47
|
if (Array.isArray(engine.categories)) {
|
|
@@ -51,6 +79,17 @@ function collectEngines(config, includeDisabled, category) {
|
|
|
51
79
|
...(includeDisabled ? { disabled: [...disabled].sort() } : {}),
|
|
52
80
|
};
|
|
53
81
|
}
|
|
82
|
+
function allEngineNames(config) {
|
|
83
|
+
const names = new Set();
|
|
84
|
+
if (Array.isArray(config.engines)) {
|
|
85
|
+
for (const engine of config.engines) {
|
|
86
|
+
if (engine && typeof engine.name === "string") {
|
|
87
|
+
names.add(engine.name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return names;
|
|
92
|
+
}
|
|
54
93
|
function formatInstanceInfo(config, includeEngines, includeDisabled, category) {
|
|
55
94
|
const categories = category
|
|
56
95
|
? namesFromCategories(config).filter((name) => name === category)
|
|
@@ -76,16 +115,19 @@ export function clearInstanceInfoCacheForTests() {
|
|
|
76
115
|
cachedConfig = null;
|
|
77
116
|
cachedBaseUrl = null;
|
|
78
117
|
}
|
|
79
|
-
|
|
118
|
+
async function fetchConfig(mcpServer, refresh = false) {
|
|
80
119
|
const base = process.env.SEARXNG_URL;
|
|
81
120
|
if (!base) {
|
|
82
|
-
return
|
|
121
|
+
return {
|
|
122
|
+
available: false,
|
|
123
|
+
message: "SEARXNG_URL is not configured; cannot fetch SearXNG /config.",
|
|
124
|
+
};
|
|
83
125
|
}
|
|
84
126
|
if (refresh) {
|
|
85
127
|
cachedConfig = null;
|
|
86
128
|
}
|
|
87
129
|
if (cachedConfig && cachedBaseUrl === base) {
|
|
88
|
-
return
|
|
130
|
+
return { available: true, config: cachedConfig };
|
|
89
131
|
}
|
|
90
132
|
const parsedBase = new URL(base.endsWith("/") ? base : `${base}/`);
|
|
91
133
|
const url = new URL("config", parsedBase);
|
|
@@ -100,14 +142,42 @@ export async function fetchInstanceInfo(mcpServer, includeEngines = false, inclu
|
|
|
100
142
|
}
|
|
101
143
|
const response = await fetch(url.toString(), requestOptions);
|
|
102
144
|
if (!response.ok) {
|
|
103
|
-
return
|
|
145
|
+
return {
|
|
146
|
+
available: false,
|
|
147
|
+
message: `SearXNG /config is unavailable: HTTP ${response.status} ${response.statusText}`,
|
|
148
|
+
status: response.status,
|
|
149
|
+
};
|
|
104
150
|
}
|
|
105
151
|
cachedConfig = await response.json();
|
|
106
152
|
cachedBaseUrl = base;
|
|
107
|
-
return
|
|
153
|
+
return { available: true, config: cachedConfig };
|
|
108
154
|
}
|
|
109
155
|
catch (error) {
|
|
110
156
|
logMessage(mcpServer, "warning", `SearXNG /config fetch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
|
-
return
|
|
157
|
+
return {
|
|
158
|
+
available: false,
|
|
159
|
+
message: "SearXNG /config is unavailable; instance capability discovery could not complete.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function getKnownEngines(mcpServer, refresh = false) {
|
|
164
|
+
const result = await fetchConfig(mcpServer, refresh);
|
|
165
|
+
if (!result.available) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return allEngineNames(result.config);
|
|
169
|
+
}
|
|
170
|
+
export async function getKnownCategories(mcpServer, refresh = false) {
|
|
171
|
+
const result = await fetchConfig(mcpServer, refresh);
|
|
172
|
+
if (!result.available) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return new Set(namesFromCategories(result.config));
|
|
176
|
+
}
|
|
177
|
+
export async function fetchInstanceInfo(mcpServer, includeEngines = false, includeDisabled = false, category, refresh = false) {
|
|
178
|
+
const result = await fetchConfig(mcpServer, refresh);
|
|
179
|
+
if (!result.available) {
|
|
180
|
+
return unavailable(result.message, result.status);
|
|
112
181
|
}
|
|
182
|
+
return formatInstanceInfo(result.config, includeEngines, includeDisabled, category);
|
|
113
183
|
}
|
package/dist/resources.js
CHANGED
|
@@ -53,10 +53,11 @@ Performs web searches using the configured SearXNG instance.
|
|
|
53
53
|
- \`safesearch\` (optional): Safe search level - 0 (none), 1 (moderate), 2 (strict)
|
|
54
54
|
- \`min_score\` (optional): Minimum relevance score from 0.0 to 1.0
|
|
55
55
|
- \`num_results\` (optional): Maximum result count from 1 to 20
|
|
56
|
-
- \`categories\` (optional): Comma-separated SearXNG categories such as "news" or "it,science"
|
|
57
|
-
- \`
|
|
56
|
+
- \`categories\` (optional): Comma-separated SearXNG categories such as "news" or "it,science"; live \`/config\` values are normalized case-insensitively when available
|
|
57
|
+
- \`engines\` (optional): Comma-separated SearXNG engine names such as "google,bing,ddg" or "semantic scholar"; live \`/config\` values are normalized case-insensitively when available
|
|
58
|
+
- \`response_format\` (optional): "text" for formatted output or "json" for raw SearXNG-shaped JSON (may include a \`warnings\` array for non-fatal issues)
|
|
58
59
|
|
|
59
|
-
Text output can include metadata sections for direct answers, spelling corrections, suggestions, and infoboxes before the result list. JSON output preserves the SearXNG response shape with filtered and sliced \`results
|
|
60
|
+
Text output can include metadata sections for direct answers, spelling corrections, suggestions, and infoboxes before the result list. JSON output preserves the SearXNG response shape with filtered and sliced \`results\`, and may include a \`warnings\` array for non-fatal issues. Unknown categories or engines are rejected when live \`/config\` validation is available; if \`/config\` is unavailable, the search proceeds with the supplied values and emits a warning.
|
|
60
61
|
|
|
61
62
|
### 2. searxng_search_suggestions
|
|
62
63
|
Returns autocomplete suggestions from the configured SearXNG instance.
|
package/dist/search.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
export declare function performWebSearch(mcpServer: McpServer, query: string, pageno?: number, time_range?: string, language?: string, safesearch?: number, min_score?: number, num_results?: number, categories?: string, response_format?: "text" | "json"): Promise<string>;
|
|
2
|
+
export declare function performWebSearch(mcpServer: McpServer, query: string, pageno?: number, time_range?: string, language?: string, safesearch?: number, min_score?: number, num_results?: number, categories?: string, engines?: string, response_format?: "text" | "json"): Promise<string>;
|
package/dist/search.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parse } from "node-html-parser";
|
|
2
|
+
import { getKnownCategories, getKnownEngines } from "./instance-info.js";
|
|
1
3
|
import { createProxyAgent, createDefaultAgent, ProxyType } from "./proxy.js";
|
|
2
4
|
import { logMessage } from "./logging.js";
|
|
3
5
|
import { MCPSearXNGError, validateEnvironment, createNetworkError, createServerError, createJSONError, createDataError, createNoResultsMessage } from "./error-handler.js";
|
|
@@ -31,9 +33,226 @@ function truncateResultContent(content, maxResultChars) {
|
|
|
31
33
|
}
|
|
32
34
|
return `${content.slice(0, maxResultChars)}…`;
|
|
33
35
|
}
|
|
36
|
+
function normalizeHtmlText(text) {
|
|
37
|
+
return text.replace(/\s+/g, " ").trim();
|
|
38
|
+
}
|
|
39
|
+
function isHtmlFallbackEnabled() {
|
|
40
|
+
return process.env.SEARXNG_HTML_FALLBACK === "true";
|
|
41
|
+
}
|
|
42
|
+
function shouldFallbackForStatus(status) {
|
|
43
|
+
return status === 403 || status === 404;
|
|
44
|
+
}
|
|
45
|
+
function buildHtmlFallbackUrl(jsonUrl) {
|
|
46
|
+
const htmlUrl = new URL(jsonUrl.toString());
|
|
47
|
+
htmlUrl.searchParams.delete("format");
|
|
48
|
+
return htmlUrl;
|
|
49
|
+
}
|
|
50
|
+
function parseHtmlSearchResults(html, query) {
|
|
51
|
+
const root = parse(html);
|
|
52
|
+
const articles = root.querySelectorAll("article.result");
|
|
53
|
+
const candidates = articles.length > 0 ? articles : root.querySelectorAll(".result");
|
|
54
|
+
const results = candidates
|
|
55
|
+
.map((entry) => {
|
|
56
|
+
const link = entry.querySelector("h3 > a") ?? entry.querySelector("h3 a") ?? entry.querySelector("a[href]");
|
|
57
|
+
if (!link) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const href = link?.getAttribute("href")?.trim();
|
|
61
|
+
if (!href) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
new URL(href);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const title = normalizeHtmlText(link.text);
|
|
71
|
+
const snippetNode = entry.querySelector("p.content") ?? entry.querySelector(".content");
|
|
72
|
+
const content = snippetNode ? normalizeHtmlText(snippetNode.text) : "";
|
|
73
|
+
return {
|
|
74
|
+
title,
|
|
75
|
+
url: href,
|
|
76
|
+
content,
|
|
77
|
+
};
|
|
78
|
+
})
|
|
79
|
+
.filter((result) => result !== undefined);
|
|
80
|
+
return {
|
|
81
|
+
query,
|
|
82
|
+
number_of_results: results.length,
|
|
83
|
+
results,
|
|
84
|
+
sourceFormat: "html",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function fetchWithSearchTimeout(mcpServer, url, requestOptions, timeoutMs, query, searxngUrl) {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
90
|
+
try {
|
|
91
|
+
logMessage(mcpServer, "info", `Making request to: ${url.toString()}`);
|
|
92
|
+
return await fetch(url.toString(), {
|
|
93
|
+
...requestOptions,
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logMessage(mcpServer, "error", `Network error during search request: ${error.message}`, { query, url: url.toString() });
|
|
99
|
+
const context = {
|
|
100
|
+
url: url.toString(),
|
|
101
|
+
searxngUrl,
|
|
102
|
+
proxyAgent: !!requestOptions.dispatcher,
|
|
103
|
+
username: process.env.AUTH_USERNAME,
|
|
104
|
+
};
|
|
105
|
+
throw createNetworkError(error, context);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function fetchHtmlFallbackSearch(mcpServer, jsonUrl, requestOptions, timeoutMs, query, searxngUrl) {
|
|
112
|
+
const htmlUrl = buildHtmlFallbackUrl(jsonUrl);
|
|
113
|
+
logMessage(mcpServer, "info", `Retrying search with HTML fallback: ${htmlUrl.toString()}`);
|
|
114
|
+
const response = await fetchWithSearchTimeout(mcpServer, htmlUrl, requestOptions, timeoutMs, query, searxngUrl);
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
let responseBody;
|
|
117
|
+
try {
|
|
118
|
+
responseBody = await response.text();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
responseBody = '[Could not read response body]';
|
|
122
|
+
}
|
|
123
|
+
const context = {
|
|
124
|
+
url: htmlUrl.toString(),
|
|
125
|
+
searxngUrl,
|
|
126
|
+
};
|
|
127
|
+
throw createServerError(response.status, response.statusText, responseBody, context);
|
|
128
|
+
}
|
|
129
|
+
const html = await response.text();
|
|
130
|
+
return parseHtmlSearchResults(html, query);
|
|
131
|
+
}
|
|
34
132
|
function hasItems(items) {
|
|
35
133
|
return Array.isArray(items) && items.length > 0;
|
|
36
134
|
}
|
|
135
|
+
function splitCommaSeparated(value) {
|
|
136
|
+
return value
|
|
137
|
+
.split(",")
|
|
138
|
+
.map((entry) => entry.trim())
|
|
139
|
+
.filter((entry) => entry !== "");
|
|
140
|
+
}
|
|
141
|
+
function buildCanonicalLookup(knownValues) {
|
|
142
|
+
const lookup = new Map();
|
|
143
|
+
for (const value of knownValues) {
|
|
144
|
+
lookup.set(value.trim().toLowerCase(), value);
|
|
145
|
+
}
|
|
146
|
+
return lookup;
|
|
147
|
+
}
|
|
148
|
+
function normalizeCommaSeparated(value, knownValues) {
|
|
149
|
+
const lookup = buildCanonicalLookup(knownValues);
|
|
150
|
+
const normalized = [];
|
|
151
|
+
const invalid = [];
|
|
152
|
+
for (const requested of splitCommaSeparated(value)) {
|
|
153
|
+
const canonical = lookup.get(requested.toLowerCase());
|
|
154
|
+
if (canonical === undefined) {
|
|
155
|
+
invalid.push(requested);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
normalized.push(canonical);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
normalized: normalized.join(","),
|
|
163
|
+
invalid,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function formatAvailableValues(label, values) {
|
|
167
|
+
const available = [...values].sort().join(", ");
|
|
168
|
+
return available === "" ? "" : ` Available ${label}: ${available}.`;
|
|
169
|
+
}
|
|
170
|
+
function createValidationError(kind, invalid, available) {
|
|
171
|
+
const label = kind === "category" ? "categories" : "engines";
|
|
172
|
+
return new MCPSearXNGError(`🔍 Invalid SearXNG ${kind} name(s): ${invalid.join(", ")}.` +
|
|
173
|
+
formatAvailableValues(label, available) +
|
|
174
|
+
` Use the searxng_instance_info tool to discover available ${label}.`);
|
|
175
|
+
}
|
|
176
|
+
async function normalizeSearchFilters(mcpServer, categories, engines) {
|
|
177
|
+
const effectiveCategories = categories !== undefined && categories.trim() !== "" ? categories : undefined;
|
|
178
|
+
const effectiveEngines = engines !== undefined && engines.trim() !== "" ? engines : undefined;
|
|
179
|
+
if (!effectiveCategories && !effectiveEngines) {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
const unavailableFilterLabel = effectiveCategories && effectiveEngines
|
|
183
|
+
? "categories and engines"
|
|
184
|
+
: effectiveCategories
|
|
185
|
+
? "categories"
|
|
186
|
+
: "engines";
|
|
187
|
+
const unavailableWarning = `${unavailableFilterLabel[0].toUpperCase()}${unavailableFilterLabel.slice(1)} were not validated or normalized because SearXNG /config is unavailable.`;
|
|
188
|
+
const unavailableNote = `Note: ${unavailableFilterLabel} were not validated or normalized (SearXNG /config unavailable).`;
|
|
189
|
+
let knownCategories;
|
|
190
|
+
let knownEngines;
|
|
191
|
+
if (effectiveCategories) {
|
|
192
|
+
knownCategories = await getKnownCategories(mcpServer);
|
|
193
|
+
if (knownCategories === null) {
|
|
194
|
+
return {
|
|
195
|
+
categories: effectiveCategories,
|
|
196
|
+
engines: effectiveEngines,
|
|
197
|
+
validationWarning: unavailableWarning,
|
|
198
|
+
validationNote: unavailableNote,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (effectiveEngines) {
|
|
203
|
+
knownEngines = await getKnownEngines(mcpServer);
|
|
204
|
+
if (knownEngines === null) {
|
|
205
|
+
return {
|
|
206
|
+
categories: effectiveCategories,
|
|
207
|
+
engines: effectiveEngines,
|
|
208
|
+
validationWarning: unavailableWarning,
|
|
209
|
+
validationNote: unavailableNote,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let normalizedCategories = effectiveCategories && knownCategories
|
|
214
|
+
? normalizeCommaSeparated(effectiveCategories, knownCategories)
|
|
215
|
+
: undefined;
|
|
216
|
+
let normalizedEngines = effectiveEngines && knownEngines
|
|
217
|
+
? normalizeCommaSeparated(effectiveEngines, knownEngines)
|
|
218
|
+
: undefined;
|
|
219
|
+
if ((normalizedCategories && normalizedCategories.invalid.length > 0) ||
|
|
220
|
+
(normalizedEngines && normalizedEngines.invalid.length > 0)) {
|
|
221
|
+
if (effectiveCategories) {
|
|
222
|
+
knownCategories = await getKnownCategories(mcpServer, true);
|
|
223
|
+
knownEngines = effectiveEngines && knownCategories !== null
|
|
224
|
+
? await getKnownEngines(mcpServer)
|
|
225
|
+
: knownEngines;
|
|
226
|
+
}
|
|
227
|
+
else if (effectiveEngines) {
|
|
228
|
+
knownEngines = await getKnownEngines(mcpServer, true);
|
|
229
|
+
}
|
|
230
|
+
if (knownCategories === null || knownEngines === null) {
|
|
231
|
+
return {
|
|
232
|
+
categories: effectiveCategories,
|
|
233
|
+
engines: effectiveEngines,
|
|
234
|
+
validationWarning: unavailableWarning,
|
|
235
|
+
validationNote: unavailableNote,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
normalizedCategories = effectiveCategories && knownCategories
|
|
239
|
+
? normalizeCommaSeparated(effectiveCategories, knownCategories)
|
|
240
|
+
: undefined;
|
|
241
|
+
normalizedEngines = effectiveEngines && knownEngines
|
|
242
|
+
? normalizeCommaSeparated(effectiveEngines, knownEngines)
|
|
243
|
+
: undefined;
|
|
244
|
+
}
|
|
245
|
+
if (normalizedCategories && normalizedCategories.invalid.length > 0 && knownCategories) {
|
|
246
|
+
throw createValidationError("category", normalizedCategories.invalid, knownCategories);
|
|
247
|
+
}
|
|
248
|
+
if (normalizedEngines && normalizedEngines.invalid.length > 0 && knownEngines) {
|
|
249
|
+
throw createValidationError("engine", normalizedEngines.invalid, knownEngines);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
categories: normalizedCategories ? normalizedCategories.normalized : undefined,
|
|
253
|
+
engines: normalizedEngines ? normalizedEngines.normalized : undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
37
256
|
function formatSearchMetadata(data) {
|
|
38
257
|
const sections = [];
|
|
39
258
|
if (hasItems(data.answers)) {
|
|
@@ -77,7 +296,7 @@ function getDefaultSafesearch(mcpServer) {
|
|
|
77
296
|
}
|
|
78
297
|
return parsed;
|
|
79
298
|
}
|
|
80
|
-
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories, response_format = "text") {
|
|
299
|
+
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories, engines, response_format = "text") {
|
|
81
300
|
const startTime = Date.now();
|
|
82
301
|
const operatorMax = getOperatorMaxResults(mcpServer);
|
|
83
302
|
const effectiveMax = operatorMax !== undefined
|
|
@@ -86,6 +305,12 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
86
305
|
const maxResultChars = getMaxResultChars(mcpServer);
|
|
87
306
|
const effectiveLanguage = language ?? getDefaultLanguage();
|
|
88
307
|
const effectiveSafesearch = safesearch !== undefined ? safesearch : getDefaultSafesearch(mcpServer);
|
|
308
|
+
const validationError = validateEnvironment();
|
|
309
|
+
if (validationError) {
|
|
310
|
+
logMessage(mcpServer, "error", "Configuration invalid");
|
|
311
|
+
throw new MCPSearXNGError(validationError);
|
|
312
|
+
}
|
|
313
|
+
const filters = await normalizeSearchFilters(mcpServer, categories, engines);
|
|
89
314
|
// Build detailed log message with all parameters
|
|
90
315
|
const searchParams = [
|
|
91
316
|
`page ${pageno}`,
|
|
@@ -94,14 +319,10 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
94
319
|
effectiveSafesearch !== undefined ? `safesearch: ${effectiveSafesearch}` : null,
|
|
95
320
|
min_score !== undefined ? `min_score: ${min_score}` : null,
|
|
96
321
|
effectiveMax !== undefined ? `num_results: ${effectiveMax}` : null,
|
|
97
|
-
categories ? `categories: ${categories}` : null,
|
|
322
|
+
filters.categories ? `categories: ${filters.categories}` : null,
|
|
323
|
+
filters.engines ? `engines: ${filters.engines}` : null,
|
|
98
324
|
].filter(Boolean).join(", ");
|
|
99
325
|
logMessage(mcpServer, "info", `Starting web search: "${query}" (${searchParams})`);
|
|
100
|
-
const validationError = validateEnvironment();
|
|
101
|
-
if (validationError) {
|
|
102
|
-
logMessage(mcpServer, "error", "Configuration invalid");
|
|
103
|
-
throw new MCPSearXNGError(validationError);
|
|
104
|
-
}
|
|
105
326
|
const searxngUrl = process.env.SEARXNG_URL;
|
|
106
327
|
const parsedUrl = new URL(searxngUrl.endsWith('/') ? searxngUrl : searxngUrl + '/');
|
|
107
328
|
const url = new URL('search', parsedUrl);
|
|
@@ -118,8 +339,11 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
118
339
|
if (effectiveSafesearch !== undefined && [0, 1, 2].includes(effectiveSafesearch)) {
|
|
119
340
|
url.searchParams.set("safesearch", effectiveSafesearch.toString());
|
|
120
341
|
}
|
|
121
|
-
if (categories) {
|
|
122
|
-
url.searchParams.set("categories", categories);
|
|
342
|
+
if (filters.categories) {
|
|
343
|
+
url.searchParams.set("categories", filters.categories);
|
|
344
|
+
}
|
|
345
|
+
if (filters.engines) {
|
|
346
|
+
url.searchParams.set("engines", filters.engines);
|
|
123
347
|
}
|
|
124
348
|
// Prepare request options with headers
|
|
125
349
|
const requestOptions = {
|
|
@@ -151,57 +375,49 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
151
375
|
}
|
|
152
376
|
// Fetch with AbortController timeout and enhanced error handling
|
|
153
377
|
const SEARCH_TIMEOUT_MS = parseInt(process.env.SEARXNG_TIMEOUT_MS ?? "10000", 10);
|
|
154
|
-
const controller = new AbortController();
|
|
155
|
-
const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
|
|
156
378
|
let response;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
response = await fetch(url.toString(), {
|
|
160
|
-
...requestOptions,
|
|
161
|
-
signal: controller.signal,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
clearTimeout(timeoutId);
|
|
166
|
-
logMessage(mcpServer, "error", `Network error during search request: ${error.message}`, { query, url: url.toString() });
|
|
167
|
-
const context = {
|
|
168
|
-
url: url.toString(),
|
|
169
|
-
searxngUrl,
|
|
170
|
-
proxyAgent: !!dispatcher,
|
|
171
|
-
username
|
|
172
|
-
};
|
|
173
|
-
throw createNetworkError(error, context);
|
|
174
|
-
}
|
|
175
|
-
clearTimeout(timeoutId);
|
|
379
|
+
response = await fetchWithSearchTimeout(mcpServer, url, requestOptions, SEARCH_TIMEOUT_MS, query, searxngUrl);
|
|
380
|
+
let data;
|
|
176
381
|
if (!response.ok) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
responseBody = await response.text();
|
|
382
|
+
if (isHtmlFallbackEnabled() && shouldFallbackForStatus(response.status)) {
|
|
383
|
+
data = await fetchHtmlFallbackSearch(mcpServer, url, requestOptions, SEARCH_TIMEOUT_MS, query, searxngUrl);
|
|
180
384
|
}
|
|
181
|
-
|
|
182
|
-
responseBody
|
|
385
|
+
else {
|
|
386
|
+
let responseBody;
|
|
387
|
+
try {
|
|
388
|
+
responseBody = await response.text();
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
responseBody = '[Could not read response body]';
|
|
392
|
+
}
|
|
393
|
+
const context = {
|
|
394
|
+
url: url.toString(),
|
|
395
|
+
searxngUrl
|
|
396
|
+
};
|
|
397
|
+
throw createServerError(response.status, response.statusText, responseBody, context);
|
|
183
398
|
}
|
|
184
|
-
const context = {
|
|
185
|
-
url: url.toString(),
|
|
186
|
-
searxngUrl
|
|
187
|
-
};
|
|
188
|
-
throw createServerError(response.status, response.statusText, responseBody, context);
|
|
189
|
-
}
|
|
190
|
-
// Parse JSON response
|
|
191
|
-
let data;
|
|
192
|
-
try {
|
|
193
|
-
data = (await response.json());
|
|
194
399
|
}
|
|
195
|
-
|
|
196
|
-
|
|
400
|
+
else {
|
|
401
|
+
// Parse JSON response
|
|
197
402
|
try {
|
|
198
|
-
|
|
403
|
+
data = (await response.json());
|
|
199
404
|
}
|
|
200
|
-
catch {
|
|
201
|
-
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (isHtmlFallbackEnabled()) {
|
|
407
|
+
data = await fetchHtmlFallbackSearch(mcpServer, url, requestOptions, SEARCH_TIMEOUT_MS, query, searxngUrl);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
let responseText;
|
|
411
|
+
try {
|
|
412
|
+
responseText = await response.text();
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
responseText = '[Could not read response text]';
|
|
416
|
+
}
|
|
417
|
+
const context = { url: url.toString() };
|
|
418
|
+
throw createJSONError(responseText, context);
|
|
419
|
+
}
|
|
202
420
|
}
|
|
203
|
-
const context = { url: url.toString() };
|
|
204
|
-
throw createJSONError(responseText, context);
|
|
205
421
|
}
|
|
206
422
|
if (!data.results) {
|
|
207
423
|
const context = { url: url.toString(), query };
|
|
@@ -213,9 +429,18 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
213
429
|
? results.slice(0, effectiveMax)
|
|
214
430
|
: results;
|
|
215
431
|
if (response_format === "json") {
|
|
216
|
-
return JSON.stringify({
|
|
432
|
+
return JSON.stringify({
|
|
433
|
+
...data,
|
|
434
|
+
results: slicedResults,
|
|
435
|
+
...(filters.validationWarning ? { warnings: [filters.validationWarning] } : {}),
|
|
436
|
+
}, null, 2);
|
|
217
437
|
}
|
|
218
438
|
const metadata = formatSearchMetadata(data);
|
|
439
|
+
const leadingSections = [
|
|
440
|
+
filters.validationNote ?? null,
|
|
441
|
+
data.sourceFormat === "html" ? "Note: Results parsed from SearXNG HTML fallback; metadata is limited." : null,
|
|
442
|
+
metadata || null,
|
|
443
|
+
].filter(Boolean).join("\n\n");
|
|
219
444
|
if (slicedResults.length === 0) {
|
|
220
445
|
const appliedFilters = [
|
|
221
446
|
min_score === undefined ? null : `min_score=${min_score}`,
|
|
@@ -224,15 +449,22 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
224
449
|
const filterNote = appliedFilters ? ` after applying ${appliedFilters}` : "";
|
|
225
450
|
logMessage(mcpServer, "info", `No results found for query: "${query}"${filterNote}`);
|
|
226
451
|
const noResultsMessage = createNoResultsMessage(query);
|
|
227
|
-
return
|
|
452
|
+
return leadingSections ? `${leadingSections}\n\n---\n\n${noResultsMessage}` : noResultsMessage;
|
|
228
453
|
}
|
|
229
454
|
const duration = Date.now() - startTime;
|
|
230
455
|
logMessage(mcpServer, "info", `Search completed: "${query}" (${searchParams}) - ${slicedResults.length} results in ${duration}ms`);
|
|
231
456
|
const formattedResults = slicedResults
|
|
232
457
|
.map((r) => {
|
|
233
|
-
const
|
|
234
|
-
|
|
458
|
+
const lines = [
|
|
459
|
+
`Title: ${r.title || ""}`,
|
|
460
|
+
`Description: ${truncateResultContent(r.content || "", maxResultChars)}`,
|
|
461
|
+
`URL: ${r.url || ""}`,
|
|
462
|
+
];
|
|
463
|
+
if (r.score !== undefined) {
|
|
464
|
+
lines.push(`Relevance Score: ${r.score.toFixed(3)}`);
|
|
465
|
+
}
|
|
466
|
+
return lines.join("\n");
|
|
235
467
|
})
|
|
236
468
|
.join("\n\n");
|
|
237
|
-
return
|
|
469
|
+
return leadingSections ? `${leadingSections}\n\n---\n\n${formattedResults}` : formattedResults;
|
|
238
470
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface SearXNGWebResult {
|
|
|
3
3
|
title: string;
|
|
4
4
|
content: string;
|
|
5
5
|
url: string;
|
|
6
|
-
score
|
|
6
|
+
score?: number;
|
|
7
7
|
engine?: string;
|
|
8
8
|
engines?: string[];
|
|
9
9
|
category?: string;
|
|
@@ -23,6 +23,7 @@ export interface SearXNGWeb {
|
|
|
23
23
|
query: string;
|
|
24
24
|
number_of_results: number;
|
|
25
25
|
results: SearXNGWebResult[];
|
|
26
|
+
sourceFormat?: "json" | "html";
|
|
26
27
|
suggestions?: string[];
|
|
27
28
|
corrections?: string[];
|
|
28
29
|
answers?: string[];
|
|
@@ -38,6 +39,7 @@ export declare function isSearXNGWebSearchArgs(args: unknown): args is {
|
|
|
38
39
|
min_score?: number;
|
|
39
40
|
num_results?: number;
|
|
40
41
|
categories?: string;
|
|
42
|
+
engines?: string;
|
|
41
43
|
response_format?: "text" | "json";
|
|
42
44
|
};
|
|
43
45
|
export declare function isSearXNGSearchSuggestionsArgs(args: unknown): args is {
|
package/dist/types.js
CHANGED
|
@@ -41,6 +41,9 @@ export function isSearXNGWebSearchArgs(args) {
|
|
|
41
41
|
if (searchArgs.categories !== undefined && typeof searchArgs.categories !== "string") {
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
|
+
if (searchArgs.engines !== undefined && typeof searchArgs.engines !== "string") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
44
47
|
if (searchArgs.response_format !== undefined &&
|
|
45
48
|
(typeof searchArgs.response_format !== "string" || !VALID_RESPONSE_FORMATS.includes(searchArgs.response_format))) {
|
|
46
49
|
return false;
|
|
@@ -132,7 +135,11 @@ export const WEB_SEARCH_TOOL = {
|
|
|
132
135
|
},
|
|
133
136
|
categories: {
|
|
134
137
|
type: "string",
|
|
135
|
-
description: "Comma-separated SearXNG categories.
|
|
138
|
+
description: "Comma-separated SearXNG categories. Values are normalized case-insensitively to canonical names from live /config; unknown values are rejected with available categories listed. If /config is unavailable, values are forwarded as-is with a warning.",
|
|
139
|
+
},
|
|
140
|
+
engines: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Comma-separated SearXNG engine names to query (e.g. 'google,bing,ddg'). Values are normalized case-insensitively to canonical names from live /config; unknown values are rejected with available engines listed. If /config is unavailable, values are forwarded as-is with a warning.",
|
|
136
143
|
},
|
|
137
144
|
response_format: {
|
|
138
145
|
type: "string",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const packageVersion = "1.
|
|
1
|
+
export declare const packageVersion = "1.7.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.
|
|
1
|
+
export const packageVersion = "1.7.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-searxng",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
|
|
5
5
|
"description": "MCP server for SearXNG integration",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,21 +55,25 @@
|
|
|
55
55
|
"express": "^5.2.1",
|
|
56
56
|
"express-rate-limit": "^8.5.2",
|
|
57
57
|
"node-html-markdown": "^2.0.0",
|
|
58
|
-
"
|
|
58
|
+
"node-html-parser": "^6.1.13",
|
|
59
|
+
"undici": "7.28.0"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
|
-
"@types/node": "22.19.
|
|
62
|
+
"@types/node": "22.19.21",
|
|
62
63
|
"@types/supertest": "^7.2.0",
|
|
63
|
-
"@typescript-eslint/eslint-plugin": "8.61.
|
|
64
|
-
"@typescript-eslint/parser": "8.61.
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "8.61.1",
|
|
65
|
+
"@typescript-eslint/parser": "8.61.1",
|
|
65
66
|
"c8": "^11.0.0",
|
|
66
67
|
"cross-env": "^10.1.0",
|
|
67
|
-
"eslint": "10.
|
|
68
|
+
"eslint": "10.5.0",
|
|
68
69
|
"eslint-plugin-security": "^4.0.0",
|
|
69
70
|
"fast-check": "^4.8.0",
|
|
70
71
|
"shx": "^0.4.0",
|
|
71
72
|
"supertest": "^7.2.2",
|
|
72
73
|
"tsx": "4.22.4",
|
|
73
74
|
"typescript": "^5.8.3"
|
|
75
|
+
},
|
|
76
|
+
"overrides": {
|
|
77
|
+
"hono": ">=4.12.25"
|
|
74
78
|
}
|
|
75
79
|
}
|