mcp-searxng 1.5.0 → 1.6.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 +2 -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 +147 -12
- package/dist/types.d.ts +1 -0
- package/dist/types.js +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
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**
|
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,4 @@
|
|
|
1
|
+
import { getKnownCategories, getKnownEngines } from "./instance-info.js";
|
|
1
2
|
import { createProxyAgent, createDefaultAgent, ProxyType } from "./proxy.js";
|
|
2
3
|
import { logMessage } from "./logging.js";
|
|
3
4
|
import { MCPSearXNGError, validateEnvironment, createNetworkError, createServerError, createJSONError, createDataError, createNoResultsMessage } from "./error-handler.js";
|
|
@@ -34,6 +35,127 @@ function truncateResultContent(content, maxResultChars) {
|
|
|
34
35
|
function hasItems(items) {
|
|
35
36
|
return Array.isArray(items) && items.length > 0;
|
|
36
37
|
}
|
|
38
|
+
function splitCommaSeparated(value) {
|
|
39
|
+
return value
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((entry) => entry.trim())
|
|
42
|
+
.filter((entry) => entry !== "");
|
|
43
|
+
}
|
|
44
|
+
function buildCanonicalLookup(knownValues) {
|
|
45
|
+
const lookup = new Map();
|
|
46
|
+
for (const value of knownValues) {
|
|
47
|
+
lookup.set(value.trim().toLowerCase(), value);
|
|
48
|
+
}
|
|
49
|
+
return lookup;
|
|
50
|
+
}
|
|
51
|
+
function normalizeCommaSeparated(value, knownValues) {
|
|
52
|
+
const lookup = buildCanonicalLookup(knownValues);
|
|
53
|
+
const normalized = [];
|
|
54
|
+
const invalid = [];
|
|
55
|
+
for (const requested of splitCommaSeparated(value)) {
|
|
56
|
+
const canonical = lookup.get(requested.toLowerCase());
|
|
57
|
+
if (canonical === undefined) {
|
|
58
|
+
invalid.push(requested);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
normalized.push(canonical);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
normalized: normalized.join(","),
|
|
66
|
+
invalid,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function formatAvailableValues(label, values) {
|
|
70
|
+
const available = [...values].sort().join(", ");
|
|
71
|
+
return available === "" ? "" : ` Available ${label}: ${available}.`;
|
|
72
|
+
}
|
|
73
|
+
function createValidationError(kind, invalid, available) {
|
|
74
|
+
const label = kind === "category" ? "categories" : "engines";
|
|
75
|
+
return new MCPSearXNGError(`🔍 Invalid SearXNG ${kind} name(s): ${invalid.join(", ")}.` +
|
|
76
|
+
formatAvailableValues(label, available) +
|
|
77
|
+
` Use the searxng_instance_info tool to discover available ${label}.`);
|
|
78
|
+
}
|
|
79
|
+
async function normalizeSearchFilters(mcpServer, categories, engines) {
|
|
80
|
+
const effectiveCategories = categories !== undefined && categories.trim() !== "" ? categories : undefined;
|
|
81
|
+
const effectiveEngines = engines !== undefined && engines.trim() !== "" ? engines : undefined;
|
|
82
|
+
if (!effectiveCategories && !effectiveEngines) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const unavailableFilterLabel = effectiveCategories && effectiveEngines
|
|
86
|
+
? "categories and engines"
|
|
87
|
+
: effectiveCategories
|
|
88
|
+
? "categories"
|
|
89
|
+
: "engines";
|
|
90
|
+
const unavailableWarning = `${unavailableFilterLabel[0].toUpperCase()}${unavailableFilterLabel.slice(1)} were not validated or normalized because SearXNG /config is unavailable.`;
|
|
91
|
+
const unavailableNote = `Note: ${unavailableFilterLabel} were not validated or normalized (SearXNG /config unavailable).`;
|
|
92
|
+
let knownCategories;
|
|
93
|
+
let knownEngines;
|
|
94
|
+
if (effectiveCategories) {
|
|
95
|
+
knownCategories = await getKnownCategories(mcpServer);
|
|
96
|
+
if (knownCategories === null) {
|
|
97
|
+
return {
|
|
98
|
+
categories: effectiveCategories,
|
|
99
|
+
engines: effectiveEngines,
|
|
100
|
+
validationWarning: unavailableWarning,
|
|
101
|
+
validationNote: unavailableNote,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (effectiveEngines) {
|
|
106
|
+
knownEngines = await getKnownEngines(mcpServer);
|
|
107
|
+
if (knownEngines === null) {
|
|
108
|
+
return {
|
|
109
|
+
categories: effectiveCategories,
|
|
110
|
+
engines: effectiveEngines,
|
|
111
|
+
validationWarning: unavailableWarning,
|
|
112
|
+
validationNote: unavailableNote,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let normalizedCategories = effectiveCategories && knownCategories
|
|
117
|
+
? normalizeCommaSeparated(effectiveCategories, knownCategories)
|
|
118
|
+
: undefined;
|
|
119
|
+
let normalizedEngines = effectiveEngines && knownEngines
|
|
120
|
+
? normalizeCommaSeparated(effectiveEngines, knownEngines)
|
|
121
|
+
: undefined;
|
|
122
|
+
if ((normalizedCategories && normalizedCategories.invalid.length > 0) ||
|
|
123
|
+
(normalizedEngines && normalizedEngines.invalid.length > 0)) {
|
|
124
|
+
if (effectiveCategories) {
|
|
125
|
+
knownCategories = await getKnownCategories(mcpServer, true);
|
|
126
|
+
knownEngines = effectiveEngines && knownCategories !== null
|
|
127
|
+
? await getKnownEngines(mcpServer)
|
|
128
|
+
: knownEngines;
|
|
129
|
+
}
|
|
130
|
+
else if (effectiveEngines) {
|
|
131
|
+
knownEngines = await getKnownEngines(mcpServer, true);
|
|
132
|
+
}
|
|
133
|
+
if (knownCategories === null || knownEngines === null) {
|
|
134
|
+
return {
|
|
135
|
+
categories: effectiveCategories,
|
|
136
|
+
engines: effectiveEngines,
|
|
137
|
+
validationWarning: unavailableWarning,
|
|
138
|
+
validationNote: unavailableNote,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
normalizedCategories = effectiveCategories && knownCategories
|
|
142
|
+
? normalizeCommaSeparated(effectiveCategories, knownCategories)
|
|
143
|
+
: undefined;
|
|
144
|
+
normalizedEngines = effectiveEngines && knownEngines
|
|
145
|
+
? normalizeCommaSeparated(effectiveEngines, knownEngines)
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|
|
148
|
+
if (normalizedCategories && normalizedCategories.invalid.length > 0 && knownCategories) {
|
|
149
|
+
throw createValidationError("category", normalizedCategories.invalid, knownCategories);
|
|
150
|
+
}
|
|
151
|
+
if (normalizedEngines && normalizedEngines.invalid.length > 0 && knownEngines) {
|
|
152
|
+
throw createValidationError("engine", normalizedEngines.invalid, knownEngines);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
categories: normalizedCategories ? normalizedCategories.normalized : undefined,
|
|
156
|
+
engines: normalizedEngines ? normalizedEngines.normalized : undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
37
159
|
function formatSearchMetadata(data) {
|
|
38
160
|
const sections = [];
|
|
39
161
|
if (hasItems(data.answers)) {
|
|
@@ -77,7 +199,7 @@ function getDefaultSafesearch(mcpServer) {
|
|
|
77
199
|
}
|
|
78
200
|
return parsed;
|
|
79
201
|
}
|
|
80
|
-
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories, response_format = "text") {
|
|
202
|
+
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories, engines, response_format = "text") {
|
|
81
203
|
const startTime = Date.now();
|
|
82
204
|
const operatorMax = getOperatorMaxResults(mcpServer);
|
|
83
205
|
const effectiveMax = operatorMax !== undefined
|
|
@@ -86,6 +208,12 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
86
208
|
const maxResultChars = getMaxResultChars(mcpServer);
|
|
87
209
|
const effectiveLanguage = language ?? getDefaultLanguage();
|
|
88
210
|
const effectiveSafesearch = safesearch !== undefined ? safesearch : getDefaultSafesearch(mcpServer);
|
|
211
|
+
const validationError = validateEnvironment();
|
|
212
|
+
if (validationError) {
|
|
213
|
+
logMessage(mcpServer, "error", "Configuration invalid");
|
|
214
|
+
throw new MCPSearXNGError(validationError);
|
|
215
|
+
}
|
|
216
|
+
const filters = await normalizeSearchFilters(mcpServer, categories, engines);
|
|
89
217
|
// Build detailed log message with all parameters
|
|
90
218
|
const searchParams = [
|
|
91
219
|
`page ${pageno}`,
|
|
@@ -94,14 +222,10 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
94
222
|
effectiveSafesearch !== undefined ? `safesearch: ${effectiveSafesearch}` : null,
|
|
95
223
|
min_score !== undefined ? `min_score: ${min_score}` : null,
|
|
96
224
|
effectiveMax !== undefined ? `num_results: ${effectiveMax}` : null,
|
|
97
|
-
categories ? `categories: ${categories}` : null,
|
|
225
|
+
filters.categories ? `categories: ${filters.categories}` : null,
|
|
226
|
+
filters.engines ? `engines: ${filters.engines}` : null,
|
|
98
227
|
].filter(Boolean).join(", ");
|
|
99
228
|
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
229
|
const searxngUrl = process.env.SEARXNG_URL;
|
|
106
230
|
const parsedUrl = new URL(searxngUrl.endsWith('/') ? searxngUrl : searxngUrl + '/');
|
|
107
231
|
const url = new URL('search', parsedUrl);
|
|
@@ -118,8 +242,11 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
118
242
|
if (effectiveSafesearch !== undefined && [0, 1, 2].includes(effectiveSafesearch)) {
|
|
119
243
|
url.searchParams.set("safesearch", effectiveSafesearch.toString());
|
|
120
244
|
}
|
|
121
|
-
if (categories) {
|
|
122
|
-
url.searchParams.set("categories", categories);
|
|
245
|
+
if (filters.categories) {
|
|
246
|
+
url.searchParams.set("categories", filters.categories);
|
|
247
|
+
}
|
|
248
|
+
if (filters.engines) {
|
|
249
|
+
url.searchParams.set("engines", filters.engines);
|
|
123
250
|
}
|
|
124
251
|
// Prepare request options with headers
|
|
125
252
|
const requestOptions = {
|
|
@@ -213,9 +340,17 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
213
340
|
? results.slice(0, effectiveMax)
|
|
214
341
|
: results;
|
|
215
342
|
if (response_format === "json") {
|
|
216
|
-
return JSON.stringify({
|
|
343
|
+
return JSON.stringify({
|
|
344
|
+
...data,
|
|
345
|
+
results: slicedResults,
|
|
346
|
+
...(filters.validationWarning ? { warnings: [filters.validationWarning] } : {}),
|
|
347
|
+
}, null, 2);
|
|
217
348
|
}
|
|
218
349
|
const metadata = formatSearchMetadata(data);
|
|
350
|
+
const leadingSections = [
|
|
351
|
+
filters.validationNote ?? null,
|
|
352
|
+
metadata || null,
|
|
353
|
+
].filter(Boolean).join("\n\n");
|
|
219
354
|
if (slicedResults.length === 0) {
|
|
220
355
|
const appliedFilters = [
|
|
221
356
|
min_score === undefined ? null : `min_score=${min_score}`,
|
|
@@ -224,7 +359,7 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
224
359
|
const filterNote = appliedFilters ? ` after applying ${appliedFilters}` : "";
|
|
225
360
|
logMessage(mcpServer, "info", `No results found for query: "${query}"${filterNote}`);
|
|
226
361
|
const noResultsMessage = createNoResultsMessage(query);
|
|
227
|
-
return
|
|
362
|
+
return leadingSections ? `${leadingSections}\n\n---\n\n${noResultsMessage}` : noResultsMessage;
|
|
228
363
|
}
|
|
229
364
|
const duration = Date.now() - startTime;
|
|
230
365
|
logMessage(mcpServer, "info", `Search completed: "${query}" (${searchParams}) - ${slicedResults.length} results in ${duration}ms`);
|
|
@@ -234,5 +369,5 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
234
369
|
return `Title: ${r.title || ""}\nDescription: ${truncateResultContent(r.content || "", maxResultChars)}\nURL: ${r.url || ""}\nRelevance Score: ${score.toFixed(3)}`;
|
|
235
370
|
})
|
|
236
371
|
.join("\n\n");
|
|
237
|
-
return
|
|
372
|
+
return leadingSections ? `${leadingSections}\n\n---\n\n${formattedResults}` : formattedResults;
|
|
238
373
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare function isSearXNGWebSearchArgs(args: unknown): args is {
|
|
|
38
38
|
min_score?: number;
|
|
39
39
|
num_results?: number;
|
|
40
40
|
categories?: string;
|
|
41
|
+
engines?: string;
|
|
41
42
|
response_format?: "text" | "json";
|
|
42
43
|
};
|
|
43
44
|
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.6.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.
|
|
1
|
+
export const packageVersion = "1.6.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-searxng",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
|
|
5
5
|
"description": "MCP server for SearXNG integration",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,5 +71,8 @@
|
|
|
71
71
|
"supertest": "^7.2.2",
|
|
72
72
|
"tsx": "4.22.4",
|
|
73
73
|
"typescript": "^5.8.3"
|
|
74
|
+
},
|
|
75
|
+
"overrides": {
|
|
76
|
+
"hono": ">=4.12.25"
|
|
74
77
|
}
|
|
75
78
|
}
|