mcp-searxng 1.1.1 → 1.2.1
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 +4 -2
- package/dist/cache.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +29 -16
- package/dist/resources.js +6 -1
- package/dist/search.d.ts +1 -1
- package/dist/search.js +8 -5
- package/dist/tls-config.js +2 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +39 -6
- package/dist/url-reader.js +59 -22
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -41,9 +41,10 @@ Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g.
|
|
|
41
41
|
- **URL Content Reading**: Advanced content extraction with pagination, section filtering, and heading extraction.
|
|
42
42
|
- **Intelligent Caching**: URL content is cached with TTL (Time-To-Live) to improve performance and reduce redundant requests.
|
|
43
43
|
- **Pagination**: Control which page of results to retrieve.
|
|
44
|
-
- **Time Filtering**: Filter results by time range (day, month, year).
|
|
44
|
+
- **Time Filtering**: Filter results by time range (day, week, month, year).
|
|
45
45
|
- **Language Selection**: Filter results by preferred language.
|
|
46
46
|
- **Safe Search**: Control content filtering level for search results.
|
|
47
|
+
- **Relevance Filtering**: Filter out low-scoring search results with `min_score`.
|
|
47
48
|
|
|
48
49
|
## How It Works
|
|
49
50
|
|
|
@@ -68,9 +69,10 @@ AI Assistant (e.g. Claude)
|
|
|
68
69
|
- Inputs:
|
|
69
70
|
- `query` (string): The search query. This string is passed to external search services.
|
|
70
71
|
- `pageno` (number, optional): Search page number, starts at 1 (default 1)
|
|
71
|
-
- `time_range` (string, optional): Filter results by time range - one of: "day", "month", "year" (default: none)
|
|
72
|
+
- `time_range` (string, optional): Filter results by time range - one of: "day", "week", "month", "year" (default: none)
|
|
72
73
|
- `language` (string, optional): Language code for results (e.g., "en", "fr", "de") or "all" (default: "all")
|
|
73
74
|
- `safesearch` (number, optional): Safe search filter level (0: None, 1: Moderate, 2: Strict) (default: instance setting)
|
|
75
|
+
- `min_score` (number, optional): Minimum relevance score from 0.0 to 1.0. Results below this score are filtered out.
|
|
74
76
|
|
|
75
77
|
- **web_url_read**
|
|
76
78
|
- Read and convert the content from a URL to markdown with advanced content extraction options
|
package/dist/cache.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
declare const packageVersion = "1.
|
|
3
|
+
declare const packageVersion = "1.2.1";
|
|
4
4
|
export { packageVersion };
|
|
5
5
|
export declare function isWebUrlReadArgs(args: unknown): args is {
|
|
6
6
|
url: string;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
2
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
6
|
import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -10,7 +12,17 @@ import { fetchAndConvertToMarkdown } from "./url-reader.js";
|
|
|
10
12
|
import { createConfigResource, createHelpResource } from "./resources.js";
|
|
11
13
|
import { createHttpServer, resolveBindHost } from "./http-server.js";
|
|
12
14
|
// Use a static version string that will be updated by the version script
|
|
13
|
-
const packageVersion = "1.
|
|
15
|
+
const packageVersion = "1.2.1";
|
|
16
|
+
const isMainModule = (() => {
|
|
17
|
+
if (process.argv[1] === undefined)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
14
26
|
// Export the version for use in other modules
|
|
15
27
|
export { packageVersion };
|
|
16
28
|
// Type guard for URL reading args
|
|
@@ -77,7 +89,7 @@ export function createMcpServer() {
|
|
|
77
89
|
if (!isSearXNGWebSearchArgs(args)) {
|
|
78
90
|
throw new Error("Invalid arguments for web search");
|
|
79
91
|
}
|
|
80
|
-
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch);
|
|
92
|
+
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch, args.min_score);
|
|
81
93
|
return {
|
|
82
94
|
content: [
|
|
83
95
|
{
|
|
@@ -238,17 +250,18 @@ async function main() {
|
|
|
238
250
|
logMessage(mcpServer, "info", `SearXNG URL: ${process.env.SEARXNG_URL || 'not configured'}`);
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
main().catch((error) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
253
|
+
if (isMainModule) {
|
|
254
|
+
// Handle uncaught errors for the CLI entrypoint.
|
|
255
|
+
process.on('uncaughtException', (error) => {
|
|
256
|
+
console.error('Uncaught Exception:', error);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
259
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
260
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|
|
263
|
+
main().catch((error) => {
|
|
264
|
+
console.error("Failed to start server:", error);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
});
|
|
267
|
+
}
|
package/dist/resources.js
CHANGED
|
@@ -43,9 +43,10 @@ Performs web searches using the configured SearXNG instance.
|
|
|
43
43
|
**Parameters:**
|
|
44
44
|
- \`query\` (required): The search query string
|
|
45
45
|
- \`pageno\` (optional): Page number (default: 1)
|
|
46
|
-
- \`time_range\` (optional): Filter by time - "day", "month", or "year"
|
|
46
|
+
- \`time_range\` (optional): Filter by time - "day", "week", "month", or "year"
|
|
47
47
|
- \`language\` (optional): Language code like "en", "fr", "de" (default: "all")
|
|
48
48
|
- \`safesearch\` (optional): Safe search level - 0 (none), 1 (moderate), 2 (strict)
|
|
49
|
+
- \`min_score\` (optional): Minimum relevance score from 0.0 to 1.0
|
|
49
50
|
|
|
50
51
|
### 2. web_url_read
|
|
51
52
|
Reads and converts web page content to Markdown format.
|
|
@@ -63,6 +64,10 @@ Reads and converts web page content to Markdown format.
|
|
|
63
64
|
- \`HTTP_PROXY\` / \`HTTPS_PROXY\`: Proxy server configuration
|
|
64
65
|
- \`NO_PROXY\` / \`no_proxy\`: Comma-separated list of hosts to bypass proxy
|
|
65
66
|
- \`MCP_HTTP_PORT\`: Enable HTTP transport on specified port
|
|
67
|
+
- \`MCP_HTTP_ALLOW_PRIVATE_URLS\`: Allow \`web_url_read\` to fetch private/internal URLs. Disabled by default in all modes.
|
|
68
|
+
|
|
69
|
+
### URL Reader Security
|
|
70
|
+
\`web_url_read\` blocks private/internal URLs and redirects to private/internal URLs by default. Set \`MCP_HTTP_ALLOW_PRIVATE_URLS=true\` only when internal URL reads are intentional.
|
|
66
71
|
|
|
67
72
|
## Transport Modes
|
|
68
73
|
|
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): Promise<string>;
|
|
2
|
+
export declare function performWebSearch(mcpServer: McpServer, query: string, pageno?: number, time_range?: string, language?: string, safesearch?: number, min_score?: number): Promise<string>;
|
package/dist/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createProxyAgent, createDefaultAgent, ProxyType } from "./proxy.js";
|
|
2
2
|
import { logMessage } from "./logging.js";
|
|
3
3
|
import { MCPSearXNGError, validateEnvironment, createNetworkError, createServerError, createJSONError, createDataError, createNoResultsMessage } from "./error-handler.js";
|
|
4
|
-
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language = "all", safesearch) {
|
|
4
|
+
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language = "all", safesearch, min_score) {
|
|
5
5
|
const startTime = Date.now();
|
|
6
6
|
// Build detailed log message with all parameters
|
|
7
7
|
const searchParams = [
|
|
@@ -23,7 +23,7 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
23
23
|
url.searchParams.set("format", "json");
|
|
24
24
|
url.searchParams.set("pageno", pageno.toString());
|
|
25
25
|
if (time_range !== undefined &&
|
|
26
|
-
["day", "month", "year"].includes(time_range)) {
|
|
26
|
+
["day", "week", "month", "year"].includes(time_range)) {
|
|
27
27
|
url.searchParams.set("time_range", time_range);
|
|
28
28
|
}
|
|
29
29
|
if (language && language !== "all") {
|
|
@@ -110,14 +110,17 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
110
110
|
const context = { url: url.toString(), query };
|
|
111
111
|
throw createDataError(data, context);
|
|
112
112
|
}
|
|
113
|
-
const results = data.results
|
|
113
|
+
const results = data.results
|
|
114
|
+
.map((result) => ({
|
|
114
115
|
title: result.title || "",
|
|
115
116
|
content: result.content || "",
|
|
116
117
|
url: result.url || "",
|
|
117
118
|
score: result.score || 0,
|
|
118
|
-
}))
|
|
119
|
+
}))
|
|
120
|
+
.filter((result) => min_score === undefined || result.score >= min_score);
|
|
119
121
|
if (results.length === 0) {
|
|
120
|
-
|
|
122
|
+
const filterNote = min_score === undefined ? "" : ` after applying min_score=${min_score}`;
|
|
123
|
+
logMessage(mcpServer, "info", `No results found for query: "${query}"${filterNote}`);
|
|
121
124
|
return createNoResultsMessage(query);
|
|
122
125
|
}
|
|
123
126
|
const duration = Date.now() - startTime;
|
package/dist/tls-config.js
CHANGED
|
@@ -22,8 +22,10 @@ export function getSystemCACerts() {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
for (const caPath of CA_BUNDLE_PATHS) {
|
|
25
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
25
26
|
if (existsSync(caPath)) {
|
|
26
27
|
try {
|
|
28
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
27
29
|
return readFileSync(caPath, "utf8");
|
|
28
30
|
}
|
|
29
31
|
catch {
|
package/dist/types.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function isSearXNGWebSearchArgs(args: unknown): args is {
|
|
|
13
13
|
time_range?: string;
|
|
14
14
|
language?: string;
|
|
15
15
|
safesearch?: number;
|
|
16
|
+
min_score?: number;
|
|
16
17
|
};
|
|
17
18
|
export declare const WEB_SEARCH_TOOL: Tool;
|
|
18
19
|
export declare const READ_URL_TOOL: Tool;
|
package/dist/types.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
const VALID_TIME_RANGES = ["day", "week", "month", "year"];
|
|
2
|
+
const VALID_SAFESEARCH_VALUES = [0, 1, 2];
|
|
1
3
|
export function isSearXNGWebSearchArgs(args) {
|
|
2
|
-
|
|
3
|
-
args
|
|
4
|
-
"query" in args
|
|
5
|
-
typeof args.query
|
|
4
|
+
if (typeof args !== "object" ||
|
|
5
|
+
args === null ||
|
|
6
|
+
!("query" in args) ||
|
|
7
|
+
typeof args.query !== "string") {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const searchArgs = args;
|
|
11
|
+
if (searchArgs.pageno !== undefined && (typeof searchArgs.pageno !== "number" || searchArgs.pageno < 1)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (searchArgs.time_range !== undefined &&
|
|
15
|
+
(typeof searchArgs.time_range !== "string" || !VALID_TIME_RANGES.includes(searchArgs.time_range))) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (searchArgs.language !== undefined && typeof searchArgs.language !== "string") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (searchArgs.safesearch !== undefined &&
|
|
22
|
+
(typeof searchArgs.safesearch !== "number" || !VALID_SAFESEARCH_VALUES.includes(searchArgs.safesearch))) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (searchArgs.min_score !== undefined &&
|
|
26
|
+
(typeof searchArgs.min_score !== "number" ||
|
|
27
|
+
Number.isNaN(searchArgs.min_score) ||
|
|
28
|
+
searchArgs.min_score < 0 ||
|
|
29
|
+
searchArgs.min_score > 1)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
6
33
|
}
|
|
7
34
|
export const WEB_SEARCH_TOOL = {
|
|
8
35
|
name: "searxng_web_search",
|
|
@@ -29,8 +56,8 @@ export const WEB_SEARCH_TOOL = {
|
|
|
29
56
|
},
|
|
30
57
|
time_range: {
|
|
31
58
|
type: "string",
|
|
32
|
-
description: "Time range of search (day, month, year)",
|
|
33
|
-
enum: ["day", "month", "year"],
|
|
59
|
+
description: "Time range of search (day, week, month, year)",
|
|
60
|
+
enum: ["day", "week", "month", "year"],
|
|
34
61
|
},
|
|
35
62
|
language: {
|
|
36
63
|
type: "string",
|
|
@@ -43,6 +70,12 @@ export const WEB_SEARCH_TOOL = {
|
|
|
43
70
|
enum: [0, 1, 2],
|
|
44
71
|
default: 0,
|
|
45
72
|
},
|
|
73
|
+
min_score: {
|
|
74
|
+
type: "number",
|
|
75
|
+
description: "Minimum relevance score threshold from 0.0 to 1.0. Results below this score are filtered out.",
|
|
76
|
+
minimum: 0,
|
|
77
|
+
maximum: 1,
|
|
78
|
+
},
|
|
46
79
|
},
|
|
47
80
|
required: ["query"],
|
|
48
81
|
},
|
package/dist/url-reader.js
CHANGED
|
@@ -6,6 +6,8 @@ import { logMessage } from "./logging.js";
|
|
|
6
6
|
import { urlCache } from "./cache.js";
|
|
7
7
|
import { getHttpSecurityConfig } from "./http-security.js";
|
|
8
8
|
import { createURLFormatError, createURLSecurityPolicyError, createNetworkError, createServerError, createContentError, createConversionError, createTimeoutError, createEmptyContentWarning, createUnexpectedError } from "./error-handler.js";
|
|
9
|
+
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
10
|
+
const MAX_REDIRECTS = 5;
|
|
9
11
|
function isPrivateHostname(hostname) {
|
|
10
12
|
const lower = hostname.toLowerCase().replace(/\.+$/, "");
|
|
11
13
|
return lower === "localhost" || lower.endsWith(".localhost");
|
|
@@ -14,7 +16,8 @@ function isPrivateIpv4(hostname) {
|
|
|
14
16
|
if (isIP(hostname) !== 4) {
|
|
15
17
|
return false;
|
|
16
18
|
}
|
|
17
|
-
return (hostname.startsWith("
|
|
19
|
+
return (hostname.startsWith("0.") ||
|
|
20
|
+
hostname.startsWith("10.") ||
|
|
18
21
|
hostname.startsWith("127.") ||
|
|
19
22
|
hostname.startsWith("192.168.") ||
|
|
20
23
|
/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname) ||
|
|
@@ -39,17 +42,28 @@ function isPrivateIPv6(hostname) {
|
|
|
39
42
|
const mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
40
43
|
if (mapped)
|
|
41
44
|
return isPrivateIpv4(mapped[1]);
|
|
45
|
+
// IPv4-mapped ::ffff:<hhhh>:<hhhh> — convert the hex segments to dotted decimal
|
|
46
|
+
const hexMapped = addr.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
47
|
+
if (hexMapped) {
|
|
48
|
+
const high = parseInt(hexMapped[1], 16);
|
|
49
|
+
const low = parseInt(hexMapped[2], 16);
|
|
50
|
+
const ipv4 = `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|
51
|
+
return isPrivateIpv4(ipv4);
|
|
52
|
+
}
|
|
42
53
|
return false;
|
|
43
54
|
}
|
|
44
55
|
function assertUrlAllowed(url) {
|
|
45
56
|
const security = getHttpSecurityConfig();
|
|
46
|
-
if (
|
|
57
|
+
if (security.allowPrivateUrls) {
|
|
47
58
|
return;
|
|
48
59
|
}
|
|
49
60
|
if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {
|
|
50
61
|
throw createURLSecurityPolicyError(url.toString());
|
|
51
62
|
}
|
|
52
63
|
}
|
|
64
|
+
function isRedirectResponse(response) {
|
|
65
|
+
return REDIRECT_STATUS_CODES.has(response.status);
|
|
66
|
+
}
|
|
53
67
|
function applyCharacterPagination(content, startChar = 0, maxLength) {
|
|
54
68
|
if (startChar >= content.length) {
|
|
55
69
|
return "";
|
|
@@ -58,18 +72,15 @@ function applyCharacterPagination(content, startChar = 0, maxLength) {
|
|
|
58
72
|
const end = maxLength ? Math.min(content.length, start + maxLength) : content.length;
|
|
59
73
|
return content.slice(start, end);
|
|
60
74
|
}
|
|
61
|
-
function escapeRegExp(str) {
|
|
62
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
63
|
-
}
|
|
64
75
|
function extractSection(markdownContent, sectionHeading) {
|
|
65
76
|
const lines = markdownContent.split('\n');
|
|
66
|
-
const
|
|
77
|
+
const normalizedHeading = sectionHeading.toLowerCase();
|
|
67
78
|
let startIndex = -1;
|
|
68
79
|
let currentLevel = 0;
|
|
69
|
-
// Find the section start
|
|
80
|
+
// Find the section start — string match avoids RegExp constructor with user input
|
|
70
81
|
for (let i = 0; i < lines.length; i++) {
|
|
71
82
|
const line = lines[i];
|
|
72
|
-
if (
|
|
83
|
+
if (/^#{1,6}\s/.test(line) && line.toLowerCase().includes(normalizedHeading)) {
|
|
73
84
|
startIndex = i;
|
|
74
85
|
currentLevel = (line.match(/^#+/) || [''])[0].length;
|
|
75
86
|
break;
|
|
@@ -93,6 +104,7 @@ function extractSection(markdownContent, sectionHeading) {
|
|
|
93
104
|
function extractParagraphRange(markdownContent, range) {
|
|
94
105
|
const paragraphs = markdownContent.split('\n\n').filter(p => p.trim().length > 0);
|
|
95
106
|
// Parse range (e.g., "1-5", "3", "10-")
|
|
107
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
96
108
|
const rangeMatch = range.match(/^(\d+)(?:-(\d*))?$/);
|
|
97
109
|
if (!rangeMatch) {
|
|
98
110
|
return "";
|
|
@@ -176,16 +188,11 @@ export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 1000
|
|
|
176
188
|
const controller = new AbortController();
|
|
177
189
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
178
190
|
try {
|
|
179
|
-
// Prepare request options with proxy support
|
|
191
|
+
// Prepare base request options with proxy support
|
|
180
192
|
const requestOptions = {
|
|
181
193
|
signal: controller.signal,
|
|
194
|
+
redirect: "manual",
|
|
182
195
|
};
|
|
183
|
-
// Add proxy or default dispatcher (includes system CA certs for TLS)
|
|
184
|
-
const proxyAgent = createProxyAgent(url, ProxyType.URL_READER);
|
|
185
|
-
const dispatcher = proxyAgent ?? createDefaultAgent();
|
|
186
|
-
if (dispatcher) {
|
|
187
|
-
requestOptions.dispatcher = dispatcher;
|
|
188
|
-
}
|
|
189
196
|
// Add User-Agent header if configured (URL_READER_USER_AGENT takes priority over USER_AGENT)
|
|
190
197
|
const userAgent = process.env.URL_READER_USER_AGENT || process.env.USER_AGENT;
|
|
191
198
|
if (userAgent) {
|
|
@@ -195,17 +202,47 @@ export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 1000
|
|
|
195
202
|
};
|
|
196
203
|
}
|
|
197
204
|
let response;
|
|
205
|
+
let currentUrl = parsedUrl;
|
|
206
|
+
let usedDispatcher = false;
|
|
198
207
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
|
|
209
|
+
// Add proxy or default dispatcher (includes system CA certs for TLS)
|
|
210
|
+
const proxyAgent = createProxyAgent(currentUrl.toString(), ProxyType.URL_READER);
|
|
211
|
+
const dispatcher = proxyAgent ?? createDefaultAgent();
|
|
212
|
+
usedDispatcher = !!dispatcher;
|
|
213
|
+
const currentRequestOptions = {
|
|
214
|
+
...requestOptions,
|
|
215
|
+
};
|
|
216
|
+
if (dispatcher) {
|
|
217
|
+
currentRequestOptions.dispatcher = dispatcher;
|
|
218
|
+
}
|
|
219
|
+
// Fetch the URL with the abort signal.
|
|
220
|
+
// Use undici's own fetch so it shares the same internal version as the
|
|
221
|
+
// Agent/ProxyAgent dispatcher — avoids the Node.js bundled-undici vs
|
|
222
|
+
// npm-undici version mismatch that breaks Content-Encoding decompression.
|
|
223
|
+
response = await undiciFetch(currentUrl.toString(), currentRequestOptions);
|
|
224
|
+
if (!isRedirectResponse(response)) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const location = response.headers.get("location");
|
|
228
|
+
if (!location) {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (redirects === MAX_REDIRECTS) {
|
|
232
|
+
throw createContentError(`Too many redirects while fetching URL: ${url}`, url);
|
|
233
|
+
}
|
|
234
|
+
const nextUrl = new URL(location, currentUrl);
|
|
235
|
+
assertUrlAllowed(nextUrl);
|
|
236
|
+
currentUrl = nextUrl;
|
|
237
|
+
}
|
|
204
238
|
}
|
|
205
239
|
catch (error) {
|
|
240
|
+
if (error.name === 'MCPSearXNGError') {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
206
243
|
const context = {
|
|
207
|
-
url,
|
|
208
|
-
proxyAgent:
|
|
244
|
+
url: currentUrl.toString(),
|
|
245
|
+
proxyAgent: usedDispatcher,
|
|
209
246
|
timeout: timeoutMs
|
|
210
247
|
};
|
|
211
248
|
throw createNetworkError(error, context);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-searxng",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
|
|
5
5
|
"description": "MCP server for SearXNG integration",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"bootstrap": "npm install && npm run build",
|
|
43
43
|
"inspector": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/index.js",
|
|
44
44
|
"lint": "eslint src __tests__",
|
|
45
|
+
"audit:deps": "npm audit --audit-level=moderate",
|
|
46
|
+
"security": "npm run lint && npm run audit:deps",
|
|
45
47
|
"postversion": "TAG=$(node scripts/update-version.js | tail -1) && git add src/index.ts .mcp/server.json && git commit --amend --no-edit && git tag -f $TAG"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
"c8": "^11.0.0",
|
|
62
64
|
"cross-env": "^10.1.0",
|
|
63
65
|
"eslint": "^10.1.0",
|
|
66
|
+
"eslint-plugin-security": "^4.0.0",
|
|
64
67
|
"shx": "^0.4.0",
|
|
65
68
|
"supertest": "^7.2.2",
|
|
66
69
|
"tsx": "^4.21.0",
|