mcp-searxng 1.8.0 → 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 +2 -0
- package/dist/error-handler.d.ts +4 -4
- package/dist/error-handler.js +4 -4
- package/dist/http-security.d.ts +1 -0
- package/dist/http-security.js +16 -0
- package/dist/http-server.js +8 -4
- package/dist/instance-info.js +10 -31
- package/dist/search.js +40 -30
- package/dist/searxng-instances.d.ts +1 -0
- package/dist/searxng-instances.js +14 -0
- package/dist/url-reader.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -239,6 +239,8 @@ By default the server uses STDIO. Set `MCP_HTTP_PORT` to enable HTTP mode:
|
|
|
239
239
|
|
|
240
240
|
**Endpoints:** `POST/GET/DELETE /mcp` (MCP protocol), `GET /health` (health check)
|
|
241
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
|
+
|
|
242
244
|
**Test it:**
|
|
243
245
|
|
|
244
246
|
```bash
|
package/dist/error-handler.d.ts
CHANGED
|
@@ -16,15 +16,15 @@ 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
29
|
/**
|
|
30
30
|
* Process-level crash handlers, registered by the CLI entrypoint (cli.ts).
|
package/dist/error-handler.js
CHANGED
|
@@ -84,11 +84,11 @@ export function createServerError(status, statusText, responseBody, context) {
|
|
|
84
84
|
}
|
|
85
85
|
return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${statusText}`);
|
|
86
86
|
}
|
|
87
|
-
export function createJSONError(responseText
|
|
87
|
+
export function createJSONError(responseText) {
|
|
88
88
|
const preview = responseText.substring(0, 100).replace(/\n/g, ' ');
|
|
89
89
|
return new MCPSearXNGError(`🔍 SearXNG Response Error: Invalid JSON format. Response: "${preview}..."`);
|
|
90
90
|
}
|
|
91
|
-
export function createDataError(
|
|
91
|
+
export function createDataError() {
|
|
92
92
|
return new MCPSearXNGError(`🔍 SearXNG Data Error: Missing results array in response`);
|
|
93
93
|
}
|
|
94
94
|
export function createNoResultsMessage(query) {
|
|
@@ -104,14 +104,14 @@ export function createURLSecurityPolicyError(url) {
|
|
|
104
104
|
export function createContentError(message, url) {
|
|
105
105
|
return new MCPSearXNGError(`📄 Content Error: ${message} (${url})`);
|
|
106
106
|
}
|
|
107
|
-
export function createConversionError(
|
|
107
|
+
export function createConversionError(url) {
|
|
108
108
|
return new MCPSearXNGError(`🔄 Conversion Error: Cannot convert HTML to Markdown (${url})`);
|
|
109
109
|
}
|
|
110
110
|
export function createTimeoutError(timeout, url) {
|
|
111
111
|
const hostname = new URL(url).hostname;
|
|
112
112
|
return new MCPSearXNGError(`⏱️ Timeout Error: ${hostname} took longer than ${timeout}ms to respond`);
|
|
113
113
|
}
|
|
114
|
-
export function createEmptyContentWarning(url
|
|
114
|
+
export function createEmptyContentWarning(url) {
|
|
115
115
|
return `📄 Content Warning: Page fetched but appears empty after conversion (${url}). May contain only media or require JavaScript.`;
|
|
116
116
|
}
|
|
117
117
|
export function createUnexpectedError(error, context) {
|
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/instance-info.js
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import { logMessage } from "./logging.js";
|
|
2
2
|
import { createDefaultAgent, createProxyAgent, ProxyType } from "./proxy.js";
|
|
3
|
-
import { getSearxngInstances } from "./searxng-instances.js";
|
|
3
|
+
import { getSearxngInstances, redactSearxngInstanceUrl } from "./searxng-instances.js";
|
|
4
4
|
const CONFIG_FAILURE_CACHE_TTL_MS = 60_000;
|
|
5
5
|
const cachedConfigs = new Map();
|
|
6
6
|
const cachedConfigFailures = new Map();
|
|
7
|
-
function redactInstanceUrl(raw) {
|
|
8
|
-
try {
|
|
9
|
-
const url = new URL(raw);
|
|
10
|
-
if (!url.username && !url.password) {
|
|
11
|
-
return raw;
|
|
12
|
-
}
|
|
13
|
-
url.username = "";
|
|
14
|
-
url.password = "";
|
|
15
|
-
return url.toString();
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return raw;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
7
|
function redactFailures(failures) {
|
|
22
8
|
return failures.map(({ sourceUrl, message, status }) => ({
|
|
23
|
-
sourceUrl:
|
|
9
|
+
sourceUrl: redactSearxngInstanceUrl(sourceUrl),
|
|
24
10
|
message,
|
|
25
11
|
...(status !== undefined ? { status } : {}),
|
|
26
12
|
}));
|
|
@@ -45,30 +31,21 @@ function categoryNamesFromEngines(config) {
|
|
|
45
31
|
}
|
|
46
32
|
return [...names];
|
|
47
33
|
}
|
|
48
|
-
function
|
|
34
|
+
function categoryNamesFromList(values) {
|
|
49
35
|
const names = new Set();
|
|
50
|
-
for (const category of
|
|
36
|
+
for (const category of values) {
|
|
51
37
|
if (typeof category === "string" && category.trim() !== "") {
|
|
52
38
|
names.add(category);
|
|
53
39
|
}
|
|
54
40
|
}
|
|
55
41
|
return [...names];
|
|
56
42
|
}
|
|
57
|
-
function categoryNamesFromObject(categories) {
|
|
58
|
-
const names = new Set();
|
|
59
|
-
for (const category of Object.keys(categories)) {
|
|
60
|
-
if (category.trim() !== "") {
|
|
61
|
-
names.add(category);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return [...names];
|
|
65
|
-
}
|
|
66
43
|
function configuredCategoryNames(config) {
|
|
67
44
|
if (Array.isArray(config.categories)) {
|
|
68
|
-
return
|
|
45
|
+
return categoryNamesFromList(config.categories);
|
|
69
46
|
}
|
|
70
47
|
if (config.categories && typeof config.categories === "object") {
|
|
71
|
-
return
|
|
48
|
+
return categoryNamesFromList(Object.keys(config.categories));
|
|
72
49
|
}
|
|
73
50
|
return [];
|
|
74
51
|
}
|
|
@@ -180,7 +157,7 @@ function formatInstanceInfo(configs, failures, includeEngines, includeDisabled,
|
|
|
180
157
|
const primary = configs[0].config;
|
|
181
158
|
const payload = {
|
|
182
159
|
available: true,
|
|
183
|
-
instancesReachable: configs.map(({ sourceUrl }) =>
|
|
160
|
+
instancesReachable: configs.map(({ sourceUrl }) => redactSearxngInstanceUrl(sourceUrl)),
|
|
184
161
|
...(failures.length > 0 ? { instancesUnreachable: redactFailures(failures) } : {}),
|
|
185
162
|
categories: aggregateCategories(configs, category),
|
|
186
163
|
defaults: {
|
|
@@ -251,7 +228,9 @@ async function requestInstanceConfig(mcpServer, base) {
|
|
|
251
228
|
return { available: true, config, sourceUrl: base };
|
|
252
229
|
}
|
|
253
230
|
catch (error) {
|
|
254
|
-
|
|
231
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
232
|
+
const safeMessage = rawMessage.replaceAll(base, redactSearxngInstanceUrl(base));
|
|
233
|
+
logMessage(mcpServer, "warning", `SearXNG /config fetch failed for ${redactSearxngInstanceUrl(base)}: ${safeMessage}`);
|
|
255
234
|
const message = "SearXNG /config is unavailable; instance capability discovery could not complete.";
|
|
256
235
|
return {
|
|
257
236
|
available: false,
|
package/dist/search.js
CHANGED
|
@@ -2,7 +2,7 @@ import { parse } from "node-html-parser";
|
|
|
2
2
|
import { getKnownCategories, getKnownEngines } from "./instance-info.js";
|
|
3
3
|
import { createProxyAgent, createDefaultAgent, ProxyType } from "./proxy.js";
|
|
4
4
|
import { logMessage } from "./logging.js";
|
|
5
|
-
import { getHealthySearxngInstances, getSearxngInstances, isSearxngFanoutEnabled, recordSearxngInstanceFailure, recordSearxngInstanceSuccess, } from "./searxng-instances.js";
|
|
5
|
+
import { getHealthySearxngInstances, getSearxngInstances, isSearxngFanoutEnabled, recordSearxngInstanceFailure, recordSearxngInstanceSuccess, redactSearxngInstanceUrl, } from "./searxng-instances.js";
|
|
6
6
|
import { MCPSearXNGError, validateEnvironment, createNetworkError, createServerError, createJSONError, createDataError, createNoResultsMessage } from "./error-handler.js";
|
|
7
7
|
function getOperatorMaxResults(mcpServer) {
|
|
8
8
|
const rawValue = process.env.SEARXNG_MAX_RESULTS;
|
|
@@ -88,22 +88,30 @@ function parseHtmlSearchResults(html, query) {
|
|
|
88
88
|
async function fetchWithSearchTimeout(mcpServer, url, requestOptions, timeoutMs, query, searxngUrl) {
|
|
89
89
|
const controller = new AbortController();
|
|
90
90
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
91
|
+
const rawUrl = url.toString();
|
|
92
|
+
const redactedUrl = redactSearxngInstanceUrl(rawUrl);
|
|
91
93
|
try {
|
|
92
|
-
logMessage(mcpServer, "info", `Making request to: ${
|
|
93
|
-
return await fetch(
|
|
94
|
+
logMessage(mcpServer, "info", `Making request to: ${redactedUrl}`);
|
|
95
|
+
return await fetch(rawUrl, {
|
|
94
96
|
...requestOptions,
|
|
95
97
|
signal: controller.signal,
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
catch (error) {
|
|
99
|
-
|
|
101
|
+
const safeMessage = typeof error?.message === "string"
|
|
102
|
+
? error.message.replaceAll(rawUrl, redactedUrl)
|
|
103
|
+
: error?.message;
|
|
104
|
+
const safeError = new Error(safeMessage);
|
|
105
|
+
safeError.code = error?.code;
|
|
106
|
+
safeError.cause = error?.cause;
|
|
107
|
+
logMessage(mcpServer, "error", `Network error during search request: ${safeMessage}`, { query, url: redactedUrl });
|
|
100
108
|
const context = {
|
|
101
|
-
url:
|
|
102
|
-
searxngUrl,
|
|
109
|
+
url: redactedUrl,
|
|
110
|
+
searxngUrl: redactSearxngInstanceUrl(searxngUrl),
|
|
103
111
|
proxyAgent: !!requestOptions.dispatcher,
|
|
104
112
|
username: process.env.AUTH_USERNAME,
|
|
105
113
|
};
|
|
106
|
-
throw createNetworkError(
|
|
114
|
+
throw createNetworkError(safeError, context);
|
|
107
115
|
}
|
|
108
116
|
finally {
|
|
109
117
|
clearTimeout(timeoutId);
|
|
@@ -111,7 +119,7 @@ async function fetchWithSearchTimeout(mcpServer, url, requestOptions, timeoutMs,
|
|
|
111
119
|
}
|
|
112
120
|
async function fetchHtmlFallbackSearch(mcpServer, jsonUrl, requestOptions, timeoutMs, query, searxngUrl) {
|
|
113
121
|
const htmlUrl = buildHtmlFallbackUrl(jsonUrl);
|
|
114
|
-
logMessage(mcpServer, "info", `Retrying search with HTML fallback: ${htmlUrl.toString()}`);
|
|
122
|
+
logMessage(mcpServer, "info", `Retrying search with HTML fallback: ${redactSearxngInstanceUrl(htmlUrl.toString())}`);
|
|
115
123
|
const response = await fetchWithSearchTimeout(mcpServer, htmlUrl, requestOptions, timeoutMs, query, searxngUrl);
|
|
116
124
|
if (!response.ok) {
|
|
117
125
|
let responseBody;
|
|
@@ -122,8 +130,8 @@ async function fetchHtmlFallbackSearch(mcpServer, jsonUrl, requestOptions, timeo
|
|
|
122
130
|
responseBody = '[Could not read response body]';
|
|
123
131
|
}
|
|
124
132
|
const context = {
|
|
125
|
-
url: htmlUrl.toString(),
|
|
126
|
-
searxngUrl,
|
|
133
|
+
url: redactSearxngInstanceUrl(htmlUrl.toString()),
|
|
134
|
+
searxngUrl: redactSearxngInstanceUrl(searxngUrl),
|
|
127
135
|
};
|
|
128
136
|
throw createServerError(response.status, response.statusText, responseBody, context);
|
|
129
137
|
}
|
|
@@ -330,36 +338,37 @@ async function fetchSearchFromInstance(mcpServer, instanceUrl, request) {
|
|
|
330
338
|
responseBody = '[Could not read response body]';
|
|
331
339
|
}
|
|
332
340
|
const context = {
|
|
333
|
-
url: url.toString(),
|
|
334
|
-
searxngUrl: instanceUrl
|
|
341
|
+
url: redactSearxngInstanceUrl(url.toString()),
|
|
342
|
+
searxngUrl: redactSearxngInstanceUrl(instanceUrl)
|
|
335
343
|
};
|
|
336
344
|
throw createServerError(response.status, response.statusText, responseBody, context);
|
|
337
345
|
}
|
|
338
346
|
}
|
|
339
347
|
else {
|
|
348
|
+
// Read the body as text once, then parse — a Response body is single-use, so
|
|
349
|
+
// calling response.text() after response.json() consumed it always throws and
|
|
350
|
+
// would leave the error preview empty.
|
|
351
|
+
let responseText;
|
|
340
352
|
try {
|
|
341
|
-
|
|
353
|
+
responseText = await response.text();
|
|
342
354
|
}
|
|
343
|
-
catch
|
|
355
|
+
catch {
|
|
356
|
+
responseText = '[Could not read response text]';
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
data = JSON.parse(responseText);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
344
362
|
if (isHtmlFallbackEnabled()) {
|
|
345
363
|
data = await fetchHtmlFallbackSearch(mcpServer, url, requestOptions, request.timeoutMs, request.query, instanceUrl);
|
|
346
364
|
}
|
|
347
365
|
else {
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
responseText = await response.text();
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
responseText = '[Could not read response text]';
|
|
354
|
-
}
|
|
355
|
-
const context = { url: url.toString() };
|
|
356
|
-
throw createJSONError(responseText, context);
|
|
366
|
+
throw createJSONError(responseText);
|
|
357
367
|
}
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
if (!data.results) {
|
|
361
|
-
|
|
362
|
-
throw createDataError(data, context);
|
|
371
|
+
throw createDataError();
|
|
363
372
|
}
|
|
364
373
|
return {
|
|
365
374
|
instanceUrl,
|
|
@@ -371,13 +380,13 @@ function hasSearchResults(data) {
|
|
|
371
380
|
}
|
|
372
381
|
function createAllInstancesFailedError(failures, skippedInstances) {
|
|
373
382
|
if (failures.length === 0 && skippedInstances.length > 0) {
|
|
374
|
-
return new MCPSearXNGError(`All configured SearXNG instances are in cooldown after repeated failures: ${skippedInstances.join(", ")}.`);
|
|
383
|
+
return new MCPSearXNGError(`All configured SearXNG instances are in cooldown after repeated failures: ${skippedInstances.map(redactSearxngInstanceUrl).join(", ")}.`);
|
|
375
384
|
}
|
|
376
385
|
const failureDetails = failures
|
|
377
|
-
.map(({ instanceUrl, error }) => `${instanceUrl}: ${error instanceof Error ? error.message : String(error)}`)
|
|
386
|
+
.map(({ instanceUrl, error }) => `${redactSearxngInstanceUrl(instanceUrl)}: ${error instanceof Error ? error.message : String(error)}`)
|
|
378
387
|
.join("; ");
|
|
379
388
|
const skippedDetails = skippedInstances.length > 0
|
|
380
|
-
? ` Skipped cooled-down instances: ${skippedInstances.join(", ")}.`
|
|
389
|
+
? ` Skipped cooled-down instances: ${skippedInstances.map(redactSearxngInstanceUrl).join(", ")}.`
|
|
381
390
|
: "";
|
|
382
391
|
return new MCPSearXNGError(`All configured SearXNG instances failed. ${failureDetails}${skippedDetails}`);
|
|
383
392
|
}
|
|
@@ -535,6 +544,7 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
535
544
|
data = multiResult.data;
|
|
536
545
|
servedBy = multiResult.servedBy;
|
|
537
546
|
}
|
|
547
|
+
const redactedServedBy = servedBy.map(redactSearxngInstanceUrl);
|
|
538
548
|
const results = data.results
|
|
539
549
|
.filter((result) => min_score === undefined || (result.score || 0) >= min_score);
|
|
540
550
|
const slicedResults = effectiveMax !== undefined
|
|
@@ -545,13 +555,13 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
545
555
|
...data,
|
|
546
556
|
results: slicedResults,
|
|
547
557
|
...(filters.validationWarning ? { warnings: [filters.validationWarning] } : {}),
|
|
548
|
-
...(includeProvenance ? { servedBy } : {}),
|
|
558
|
+
...(includeProvenance ? { servedBy: redactedServedBy } : {}),
|
|
549
559
|
}, null, 2);
|
|
550
560
|
}
|
|
551
561
|
const metadata = formatSearchMetadata(data);
|
|
552
562
|
const leadingSections = [
|
|
553
563
|
includeProvenance
|
|
554
|
-
? `Served by SearXNG ${
|
|
564
|
+
? `Served by SearXNG ${redactedServedBy.length === 1 ? "instance" : "instances"}: ${redactedServedBy.join(", ")}`
|
|
555
565
|
: null,
|
|
556
566
|
filters.validationNote ?? null,
|
|
557
567
|
data.sourceFormat === "html" ? "Note: Results parsed from SearXNG HTML fallback; metadata is limited." : null,
|
|
@@ -2,6 +2,7 @@ export declare function parseSearxngUrls(raw?: string | undefined): string[];
|
|
|
2
2
|
export declare function getSearxngInstances(): string[];
|
|
3
3
|
export declare function getPrimarySearxngInstance(): string | undefined;
|
|
4
4
|
export declare function validateSearxngInstanceUrl(value: string): string | null;
|
|
5
|
+
export declare function redactSearxngInstanceUrl(raw: string): string;
|
|
5
6
|
export declare function isSearxngFanoutEnabled(): boolean;
|
|
6
7
|
export declare function recordSearxngInstanceFailure(instanceUrl: string, now?: number): void;
|
|
7
8
|
export declare function recordSearxngInstanceSuccess(instanceUrl: string): void;
|
|
@@ -39,6 +39,20 @@ export function validateSearxngInstanceUrl(value) {
|
|
|
39
39
|
}
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
+
export function redactSearxngInstanceUrl(raw) {
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(raw);
|
|
45
|
+
if (!url.username && !url.password) {
|
|
46
|
+
return raw;
|
|
47
|
+
}
|
|
48
|
+
url.username = "";
|
|
49
|
+
url.password = "";
|
|
50
|
+
return url.toString();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return raw.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/]*@/, "$1");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
42
56
|
export function isSearxngFanoutEnabled() {
|
|
43
57
|
return process.env.SEARXNG_FANOUT === "true";
|
|
44
58
|
}
|
package/dist/url-reader.js
CHANGED
|
@@ -326,13 +326,13 @@ export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 1000
|
|
|
326
326
|
try {
|
|
327
327
|
markdownContent = NodeHtmlMarkdown.translate(htmlContent);
|
|
328
328
|
}
|
|
329
|
-
catch
|
|
330
|
-
throw createConversionError(
|
|
329
|
+
catch {
|
|
330
|
+
throw createConversionError(url);
|
|
331
331
|
}
|
|
332
332
|
if (!markdownContent || markdownContent.trim().length === 0) {
|
|
333
333
|
logMessage(mcpServer, "warning", `Empty content after conversion: ${url}`);
|
|
334
334
|
// DON'T cache empty/failed conversions - return warning directly
|
|
335
|
-
return createEmptyContentWarning(url
|
|
335
|
+
return createEmptyContentWarning(url);
|
|
336
336
|
}
|
|
337
337
|
// Only cache successful markdown conversion
|
|
338
338
|
urlCache.set(url, htmlContent, markdownContent);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const packageVersion = "1.
|
|
1
|
+
export declare const packageVersion = "1.9.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.
|
|
1
|
+
export const packageVersion = "1.9.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-searxng",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
|
|
5
5
|
"description": "MCP server for SearXNG integration",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "22.20.0",
|
|
63
63
|
"@types/supertest": "^7.2.0",
|
|
64
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
65
|
-
"@typescript-eslint/parser": "8.
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "8.62.0",
|
|
65
|
+
"@typescript-eslint/parser": "8.62.0",
|
|
66
66
|
"c8": "^11.0.0",
|
|
67
67
|
"cross-env": "^10.1.0",
|
|
68
|
-
"eslint": "10.
|
|
68
|
+
"eslint": "10.6.0",
|
|
69
69
|
"eslint-plugin-security": "^4.0.0",
|
|
70
70
|
"fast-check": "^4.8.0",
|
|
71
71
|
"shx": "^0.4.0",
|