mcp-searxng 1.7.2 → 1.9.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 +12 -9
- package/dist/cli.js +3 -8
- package/dist/error-handler.d.ts +13 -4
- package/dist/error-handler.js +26 -13
- package/dist/http-security.d.ts +1 -0
- package/dist/http-security.js +16 -0
- package/dist/http-server.js +8 -4
- package/dist/index.js +7 -4
- package/dist/instance-info.js +197 -67
- package/dist/resources.js +5 -5
- package/dist/search.js +260 -134
- package/dist/searxng-instances.d.ts +11 -0
- package/dist/searxng-instances.js +82 -0
- package/dist/suggestions.js +2 -1
- package/dist/tls-config.d.ts +15 -2
- package/dist/tls-config.js +10 -9
- package/dist/types.d.ts +1 -1
- package/dist/types.js +8 -8
- package/dist/url-reader.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -38,11 +38,12 @@ Add to your MCP client configuration (e.g. `claude_desktop_config.json`):
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g. `https://searxng.example.com`).
|
|
41
|
+
Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g. `https://searxng.example.com`). You can also provide interchangeable replicas as a semicolon-separated list, e.g. `https://one.example.com;https://two.example.com`.
|
|
42
42
|
|
|
43
43
|
## Features
|
|
44
44
|
|
|
45
45
|
- **Web Search**: General queries, news, articles, with pagination.
|
|
46
|
+
- **Instance Failover**: Configure multiple interchangeable SearXNG replicas in `SEARXNG_URL`; searches fail over by default and can optionally fan out in parallel.
|
|
46
47
|
- **Structured Search Output**: Choose formatted text or raw SearXNG-shaped JSON with `response_format`.
|
|
47
48
|
- **Direct Answers & Metadata**: Text results surface SearXNG answers, corrections, suggestions, and infoboxes before result lists.
|
|
48
49
|
- **Search Suggestions**: Query autocomplete via SearXNG's `/autocompleter` endpoint.
|
|
@@ -68,9 +69,9 @@ Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g.
|
|
|
68
69
|
|
|
69
70
|
## How It Works
|
|
70
71
|
|
|
71
|
-
`mcp-searxng` is a standalone MCP server — a separate Node.js process that your AI assistant connects to for web search. It queries
|
|
72
|
+
`mcp-searxng` is a standalone MCP server — a separate Node.js process that your AI assistant connects to for web search. It queries one SearXNG instance, or a semicolon-separated list of interchangeable SearXNG replicas, via the HTTP JSON API.
|
|
72
73
|
|
|
73
|
-
> **Not a SearXNG plugin:** This project cannot be installed as a native SearXNG plugin. Point it at any existing SearXNG instance by setting `SEARXNG_URL`.
|
|
74
|
+
> **Not a SearXNG plugin:** This project cannot be installed as a native SearXNG plugin. Point it at any existing SearXNG instance, or interchangeable replica list, by setting `SEARXNG_URL`.
|
|
74
75
|
|
|
75
76
|
```
|
|
76
77
|
AI Assistant (e.g. Claude)
|
|
@@ -79,7 +80,7 @@ AI Assistant (e.g. Claude)
|
|
|
79
80
|
mcp-searxng (this project — Node.js process)
|
|
80
81
|
│ HTTP JSON API (SEARXNG_URL)
|
|
81
82
|
▼
|
|
82
|
-
SearXNG instance
|
|
83
|
+
SearXNG instance(s)
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
## Tools
|
|
@@ -91,11 +92,11 @@ AI Assistant (e.g. Claude)
|
|
|
91
92
|
- `pageno` (number, optional): Search page number, starts at 1 (default 1)
|
|
92
93
|
- `time_range` (string, optional): Filter results by time range - one of: "day", "week", "month", "year" (default: none)
|
|
93
94
|
- `language` (string, optional): Language code for results (e.g., "en", "fr", "de") or "all" (default: "all")
|
|
94
|
-
- `safesearch` (
|
|
95
|
+
- `safesearch` (string enum, optional): Safe search filter level, one of `"0"` (None), `"1"` (Moderate), or `"2"` (Strict). Legacy numeric values `0`, `1`, and `2` are still accepted for backward compatibility. (default: instance setting)
|
|
95
96
|
- `min_score` (number, optional): Minimum relevance score from 0.0 to 1.0. Results below this score are filtered out.
|
|
96
97
|
- `num_results` (number, optional): Maximum number of results to return, from 1 to 20. `SEARXNG_MAX_RESULTS` applies as an operator ceiling.
|
|
97
|
-
- `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`).
|
|
98
|
-
- `engines` (string, optional): Comma-separated SearXNG engine names (e.g. `"google,bing,ddg"`, `"semantic scholar"`).
|
|
98
|
+
- `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`). Live `/config` capabilities are aggregated across reachable instances; prefer `searxng_instance_info` `categories.common` for consistent multi-instance results. Known values are trimmed and normalized case-insensitively; unknown values are forwarded trimmed so SearXNG can ignore or honor them. If `/config` is unavailable, values are forwarded as-is with a warning. If omitted, each instance uses its server-side default.
|
|
99
|
+
- `engines` (string, optional): Comma-separated SearXNG engine names (e.g. `"google,bing,ddg"`, `"semantic scholar"`). Live `/config` capabilities are aggregated across reachable instances; prefer `searxng_instance_info` `engines.common.enabled` for consistent multi-instance results. Known values are trimmed and normalized case-insensitively, including engines disabled by default; unknown values are forwarded trimmed so SearXNG can ignore or honor them. If `/config` is unavailable, values are forwarded as-is with a warning. If omitted, each instance uses its server-side default.
|
|
99
100
|
- `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"`)
|
|
100
101
|
|
|
101
102
|
- **searxng_search_suggestions**
|
|
@@ -105,7 +106,7 @@ AI Assistant (e.g. Claude)
|
|
|
105
106
|
- `language` (string, optional): Language code for suggestions (e.g., "en", "fr", "de") or "all" (default: "all")
|
|
106
107
|
|
|
107
108
|
- **searxng_instance_info**
|
|
108
|
-
- Discover categories, engines, defaults, locales, and plugins exposed by
|
|
109
|
+
- Discover categories, engines, defaults, locales, and plugins exposed by all reachable configured SearXNG instances. The response reports `common` values present on every reachable instance and `available` values present on at least one reachable instance.
|
|
109
110
|
- Inputs:
|
|
110
111
|
- `includeEngines` (boolean, optional): Include enabled engine names in the response. (default: false)
|
|
111
112
|
- `includeDisabled` (boolean, optional): Include disabled engine names when `includeEngines` is true. (default: false)
|
|
@@ -238,6 +239,8 @@ By default the server uses STDIO. Set `MCP_HTTP_PORT` to enable HTTP mode:
|
|
|
238
239
|
|
|
239
240
|
**Endpoints:** `POST/GET/DELETE /mcp` (MCP protocol), `GET /health` (health check)
|
|
240
241
|
|
|
242
|
+
For reverse-proxy deployments, see [CONFIGURATION.md](CONFIGURATION.md) for `MCP_HTTP_TRUST_PROXY` so rate limiting and logs use the correct client IP.
|
|
243
|
+
|
|
241
244
|
**Test it:**
|
|
242
245
|
|
|
243
246
|
```bash
|
|
@@ -249,7 +252,7 @@ curl http://localhost:3000/health
|
|
|
249
252
|
|
|
250
253
|
## Configuration
|
|
251
254
|
|
|
252
|
-
Set `SEARXNG_URL` to your SearXNG instance URL. All other variables are optional.
|
|
255
|
+
Set `SEARXNG_URL` to your SearXNG instance URL. For failover, set it to semicolon-separated interchangeable replica URLs. Set `SEARXNG_FANOUT=true` to query all healthy replicas in parallel and merge results. All other variables are optional.
|
|
253
256
|
|
|
254
257
|
Full environment variable reference: [CONFIGURATION.md](CONFIGURATION.md)
|
|
255
258
|
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from "./index.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
});
|
|
7
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
8
|
-
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
9
|
-
process.exit(1);
|
|
10
|
-
});
|
|
3
|
+
import { handleUncaughtException, handleUnhandledRejection } from "./error-handler.js";
|
|
4
|
+
process.on('uncaughtException', handleUncaughtException);
|
|
5
|
+
process.on('unhandledRejection', handleUnhandledRejection);
|
|
11
6
|
main().catch((error) => {
|
|
12
7
|
console.error("Failed to start server:", error);
|
|
13
8
|
process.exit(1);
|
package/dist/error-handler.d.ts
CHANGED
|
@@ -16,14 +16,23 @@ export declare class MCPSearXNGError extends Error {
|
|
|
16
16
|
export declare function createConfigurationError(message: string): MCPSearXNGError;
|
|
17
17
|
export declare function createNetworkError(error: any, context: ErrorContext): MCPSearXNGError;
|
|
18
18
|
export declare function createServerError(status: number, statusText: string, responseBody: string, context: ErrorContext): MCPSearXNGError;
|
|
19
|
-
export declare function createJSONError(responseText: string
|
|
20
|
-
export declare function createDataError(
|
|
19
|
+
export declare function createJSONError(responseText: string): MCPSearXNGError;
|
|
20
|
+
export declare function createDataError(): MCPSearXNGError;
|
|
21
21
|
export declare function createNoResultsMessage(query: string): string;
|
|
22
22
|
export declare function createURLFormatError(url: string): MCPSearXNGError;
|
|
23
23
|
export declare function createURLSecurityPolicyError(url: string): MCPSearXNGError;
|
|
24
24
|
export declare function createContentError(message: string, url: string): MCPSearXNGError;
|
|
25
|
-
export declare function createConversionError(
|
|
25
|
+
export declare function createConversionError(url: string): MCPSearXNGError;
|
|
26
26
|
export declare function createTimeoutError(timeout: number, url: string): MCPSearXNGError;
|
|
27
|
-
export declare function createEmptyContentWarning(url: string
|
|
27
|
+
export declare function createEmptyContentWarning(url: string): string;
|
|
28
28
|
export declare function createUnexpectedError(error: any, context: ErrorContext): MCPSearXNGError;
|
|
29
|
+
/**
|
|
30
|
+
* Process-level crash handlers, registered by the CLI entrypoint (cli.ts).
|
|
31
|
+
*
|
|
32
|
+
* Extracted here so the logic is unit-testable: cli.ts calls main() at import
|
|
33
|
+
* time (it must always start the server — see issue #91), so it cannot be
|
|
34
|
+
* imported to test these in place.
|
|
35
|
+
*/
|
|
36
|
+
export declare function handleUncaughtException(error: unknown): void;
|
|
37
|
+
export declare function handleUnhandledRejection(reason: unknown, promise: Promise<unknown>): void;
|
|
29
38
|
export declare function validateEnvironment(): string | null;
|
package/dist/error-handler.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Concise error handling for MCP SearXNG server
|
|
3
3
|
* Provides clear, focused error messages that identify the root cause
|
|
4
4
|
*/
|
|
5
|
+
import { parseSearxngUrls, validateSearxngInstanceUrl } from "./searxng-instances.js";
|
|
5
6
|
export class MCPSearXNGError extends Error {
|
|
6
7
|
constructor(message) {
|
|
7
8
|
super(message);
|
|
@@ -83,11 +84,11 @@ export function createServerError(status, statusText, responseBody, context) {
|
|
|
83
84
|
}
|
|
84
85
|
return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${statusText}`);
|
|
85
86
|
}
|
|
86
|
-
export function createJSONError(responseText
|
|
87
|
+
export function createJSONError(responseText) {
|
|
87
88
|
const preview = responseText.substring(0, 100).replace(/\n/g, ' ');
|
|
88
89
|
return new MCPSearXNGError(`🔍 SearXNG Response Error: Invalid JSON format. Response: "${preview}..."`);
|
|
89
90
|
}
|
|
90
|
-
export function createDataError(
|
|
91
|
+
export function createDataError() {
|
|
91
92
|
return new MCPSearXNGError(`🔍 SearXNG Data Error: Missing results array in response`);
|
|
92
93
|
}
|
|
93
94
|
export function createNoResultsMessage(query) {
|
|
@@ -103,35 +104,47 @@ export function createURLSecurityPolicyError(url) {
|
|
|
103
104
|
export function createContentError(message, url) {
|
|
104
105
|
return new MCPSearXNGError(`📄 Content Error: ${message} (${url})`);
|
|
105
106
|
}
|
|
106
|
-
export function createConversionError(
|
|
107
|
+
export function createConversionError(url) {
|
|
107
108
|
return new MCPSearXNGError(`🔄 Conversion Error: Cannot convert HTML to Markdown (${url})`);
|
|
108
109
|
}
|
|
109
110
|
export function createTimeoutError(timeout, url) {
|
|
110
111
|
const hostname = new URL(url).hostname;
|
|
111
112
|
return new MCPSearXNGError(`⏱️ Timeout Error: ${hostname} took longer than ${timeout}ms to respond`);
|
|
112
113
|
}
|
|
113
|
-
export function createEmptyContentWarning(url
|
|
114
|
+
export function createEmptyContentWarning(url) {
|
|
114
115
|
return `📄 Content Warning: Page fetched but appears empty after conversion (${url}). May contain only media or require JavaScript.`;
|
|
115
116
|
}
|
|
116
117
|
export function createUnexpectedError(error, context) {
|
|
117
118
|
return new MCPSearXNGError(`❓ Unexpected Error: ${error.message || String(error)}`);
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Process-level crash handlers, registered by the CLI entrypoint (cli.ts).
|
|
122
|
+
*
|
|
123
|
+
* Extracted here so the logic is unit-testable: cli.ts calls main() at import
|
|
124
|
+
* time (it must always start the server — see issue #91), so it cannot be
|
|
125
|
+
* imported to test these in place.
|
|
126
|
+
*/
|
|
127
|
+
export function handleUncaughtException(error) {
|
|
128
|
+
console.error('Uncaught Exception:', error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
export function handleUnhandledRejection(reason, promise) {
|
|
132
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
119
135
|
export function validateEnvironment() {
|
|
120
136
|
const issues = [];
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
137
|
+
const searxngUrls = parseSearxngUrls();
|
|
138
|
+
if (searxngUrls.length === 0) {
|
|
123
139
|
issues.push("SEARXNG_URL not set");
|
|
124
140
|
}
|
|
125
141
|
else {
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
issues.push(
|
|
142
|
+
for (const searxngUrl of searxngUrls) {
|
|
143
|
+
const validationError = validateSearxngInstanceUrl(searxngUrl);
|
|
144
|
+
if (validationError) {
|
|
145
|
+
issues.push(validationError);
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
issues.push(`SEARXNG_URL invalid format: ${searxngUrl}`);
|
|
134
|
-
}
|
|
135
148
|
}
|
|
136
149
|
const authUsername = process.env.AUTH_USERNAME;
|
|
137
150
|
const authPassword = process.env.AUTH_PASSWORD;
|
package/dist/http-security.d.ts
CHANGED
package/dist/http-security.js
CHANGED
|
@@ -7,6 +7,21 @@ function parseCsv(value) {
|
|
|
7
7
|
.map((item) => item.trim())
|
|
8
8
|
.filter(Boolean);
|
|
9
9
|
}
|
|
10
|
+
function parseTrustProxy(value) {
|
|
11
|
+
const trimmed = value?.trim();
|
|
12
|
+
// Treat "0" as disabled (false): operators use 0 to turn numeric knobs off,
|
|
13
|
+
// and Express would otherwise mis-parse the string "0" as a bogus trust subnet.
|
|
14
|
+
if (!trimmed || trimmed === "false" || trimmed === "0") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (trimmed === "true") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (/^[1-9]\d*$/.test(trimmed)) {
|
|
21
|
+
return Number(trimmed);
|
|
22
|
+
}
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
10
25
|
export function getHttpSecurityConfig() {
|
|
11
26
|
const harden = isEnabled(process.env.MCP_HTTP_HARDEN);
|
|
12
27
|
const authToken = process.env.MCP_HTTP_AUTH_TOKEN;
|
|
@@ -20,6 +35,7 @@ export function getHttpSecurityConfig() {
|
|
|
20
35
|
allowedOrigins,
|
|
21
36
|
enableDnsRebindingProtection: harden,
|
|
22
37
|
allowedHosts: allowedHosts.length > 0 ? allowedHosts : ["127.0.0.1", "localhost"],
|
|
38
|
+
trustProxy: parseTrustProxy(process.env.MCP_HTTP_TRUST_PROXY),
|
|
23
39
|
exposeFullConfig: isEnabled(process.env.MCP_HTTP_EXPOSE_FULL_CONFIG),
|
|
24
40
|
allowPrivateUrls: isEnabled(process.env.MCP_HTTP_ALLOW_PRIVATE_URLS),
|
|
25
41
|
};
|
package/dist/http-server.js
CHANGED
|
@@ -55,6 +55,9 @@ export async function createHttpServer(createMcpServer) {
|
|
|
55
55
|
const app = express();
|
|
56
56
|
const security = getHttpSecurityConfig();
|
|
57
57
|
validateHttpSecurityConfig(security);
|
|
58
|
+
if (security.trustProxy !== false) {
|
|
59
|
+
app.set('trust proxy', security.trustProxy);
|
|
60
|
+
}
|
|
58
61
|
app.use(express.json());
|
|
59
62
|
// Add CORS support for web clients
|
|
60
63
|
app.use(cors({
|
|
@@ -97,7 +100,7 @@ export async function createHttpServer(createMcpServer) {
|
|
|
97
100
|
mcpServer = session.mcpServer;
|
|
98
101
|
logMessage(mcpServer, "debug", `Reusing session: ${sessionId}`);
|
|
99
102
|
}
|
|
100
|
-
else if (
|
|
103
|
+
else if (isInitializeRequest(req.body)) {
|
|
101
104
|
// New initialization request — create fresh McpServer and transport
|
|
102
105
|
mcpServer = createMcpServer();
|
|
103
106
|
transport = new StreamableHTTPServerTransport({
|
|
@@ -129,11 +132,12 @@ export async function createHttpServer(createMcpServer) {
|
|
|
129
132
|
contentType: req.headers['content-type'],
|
|
130
133
|
accept: req.headers['accept']
|
|
131
134
|
});
|
|
132
|
-
|
|
135
|
+
const sessionNotFound = Boolean(sessionId);
|
|
136
|
+
res.status(sessionNotFound ? 404 : 400).json({
|
|
133
137
|
jsonrpc: '2.0',
|
|
134
138
|
error: {
|
|
135
|
-
code: -32000,
|
|
136
|
-
message: 'Bad Request: No valid session ID provided',
|
|
139
|
+
code: sessionNotFound ? -32001 : -32000,
|
|
140
|
+
message: sessionNotFound ? 'Session not found' : 'Bad Request: No valid session ID provided',
|
|
137
141
|
},
|
|
138
142
|
id: null,
|
|
139
143
|
});
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { fetchInstanceInfo } from "./instance-info.js";
|
|
|
10
10
|
import { fetchAndConvertToMarkdown } from "./url-reader.js";
|
|
11
11
|
import { createConfigResource, createHelpResource } from "./resources.js";
|
|
12
12
|
import { createHttpServer, resolveBindHost } from "./http-server.js";
|
|
13
|
+
import { getSearxngInstances } from "./searxng-instances.js";
|
|
13
14
|
import { packageVersion } from "./version.js";
|
|
14
15
|
// Type guard for URL reading args
|
|
15
16
|
export function isWebUrlReadArgs(args) {
|
|
@@ -104,7 +105,7 @@ export function createMcpServer() {
|
|
|
104
105
|
if (!isSearXNGWebSearchArgs(args)) {
|
|
105
106
|
throw new Error("Invalid arguments for web search");
|
|
106
107
|
}
|
|
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
|
+
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch === undefined ? undefined : Number(args.safesearch), args.min_score, args.num_results, args.categories, args.engines, args.response_format);
|
|
108
109
|
return {
|
|
109
110
|
content: [
|
|
110
111
|
{
|
|
@@ -277,8 +278,9 @@ export async function main() {
|
|
|
277
278
|
// Show helpful message when running in terminal
|
|
278
279
|
if (process.stdin.isTTY) {
|
|
279
280
|
console.error(`🔍 MCP SearXNG Server v${packageVersion} - Ready`);
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
const searxngInstances = getSearxngInstances();
|
|
282
|
+
if (searxngInstances.length > 0) {
|
|
283
|
+
console.error(`🌐 SearXNG URLs: ${searxngInstances.join("; ")}`);
|
|
282
284
|
}
|
|
283
285
|
else {
|
|
284
286
|
console.error("⚠️ SEARXNG_URL not set — configure it before using search tools");
|
|
@@ -291,6 +293,7 @@ export async function main() {
|
|
|
291
293
|
logMessage(mcpServer, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
|
|
292
294
|
logMessage(mcpServer, "info", `Log level: ${getCurrentLogLevel()}`);
|
|
293
295
|
logMessage(mcpServer, "info", `Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
294
|
-
|
|
296
|
+
const searxngInstances = getSearxngInstances();
|
|
297
|
+
logMessage(mcpServer, "info", `SearXNG URLs: ${searxngInstances.length > 0 ? searxngInstances.join("; ") : 'not configured'}`);
|
|
295
298
|
}
|
|
296
299
|
}
|