mcp-searxng 1.3.3 → 1.4.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 CHANGED
@@ -10,7 +10,7 @@
10
10
  [![Docker Pulls](https://img.shields.io/docker/pulls/isokoliuk/mcp-searxng?style=flat-square&logo=docker)](https://hub.docker.com/r/isokoliuk/mcp-searxng)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
12
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ihor-sokoliuk/mcp-searxng/badge)](https://scorecard.dev/viewer/?uri=github.com/ihor-sokoliuk/mcp-searxng)
13
- [![OpenSSF Baseline](https://www.bestpractices.dev/projects/13143/baseline)](https://www.bestpractices.dev/projects/13143)
13
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13143/badge)](https://www.bestpractices.dev/projects/13143)
14
14
  [![mcp-searxng MCP server](https://glama.ai/mcp/servers/ihor-sokoliuk/mcp-searxng/badges/score.svg)](https://glama.ai/mcp/servers/ihor-sokoliuk/mcp-searxng)
15
15
 
16
16
  An [MCP server](https://modelcontextprotocol.io/introduction) that integrates the [SearXNG](https://docs.searxng.org) API, giving AI assistants web search capabilities.
@@ -86,6 +86,8 @@ AI Assistant (e.g. Claude)
86
86
  - `language` (string, optional): Language code for results (e.g., "en", "fr", "de") or "all" (default: "all")
87
87
  - `safesearch` (number, optional): Safe search filter level (0: None, 1: Moderate, 2: Strict) (default: instance setting)
88
88
  - `min_score` (number, optional): Minimum relevance score from 0.0 to 1.0. Results below this score are filtered out.
89
+ - `num_results` (number, optional): Maximum number of results to return, from 1 to 20. `SEARXNG_MAX_RESULTS` applies as an operator ceiling.
90
+ - `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`). Supported values: `general`, `news`, `images`, `videos`, `it`, `science`, `files`, `social media`. Default: SearXNG instance default (usually `general`).
89
91
 
90
92
  - **web_url_read**
91
93
  - Read and convert the content from a URL to markdown with advanced content extraction options
@@ -130,6 +132,8 @@ npm install -g mcp-searxng
130
132
  docker pull isokoliuk/mcp-searxng:latest
131
133
  ```
132
134
 
135
+ Image signatures can be verified with Cosign — see [SECURITY.md](SECURITY.md) for instructions.
136
+
133
137
  ```json
134
138
  {
135
139
  "mcpServers": {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  // Import modularized functionality
5
- import { WEB_SEARCH_TOOL, READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
5
+ import { WEB_SEARCH_TOOL, READ_URL_TOOL, LITE_WEB_SEARCH_TOOL, LITE_READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
6
6
  import { logMessage, setLogLevel, getCurrentLogLevel } from "./logging.js";
7
7
  import { performWebSearch } from "./search.js";
8
8
  import { fetchAndConvertToMarkdown } from "./url-reader.js";
@@ -41,6 +41,30 @@ export function isWebUrlReadArgs(args) {
41
41
  }
42
42
  return true;
43
43
  }
44
+ function getFetchTimeoutMs(mcpServer) {
45
+ const rawValue = process.env.FETCH_TIMEOUT_MS;
46
+ if (rawValue === undefined || rawValue.trim() === "") {
47
+ return 10000;
48
+ }
49
+ const parsed = parseInt(rawValue, 10);
50
+ if (Number.isNaN(parsed) || parsed <= 0) {
51
+ logMessage(mcpServer, "warning", `Ignoring invalid FETCH_TIMEOUT_MS="${rawValue}". Expected a positive integer. Using default 10000.`);
52
+ return 10000;
53
+ }
54
+ return parsed;
55
+ }
56
+ function getDefaultUrlReadMaxChars(mcpServer) {
57
+ const rawValue = process.env.URL_READ_MAX_CHARS;
58
+ if (rawValue === undefined || rawValue.trim() === "") {
59
+ return undefined;
60
+ }
61
+ const parsed = parseInt(rawValue, 10);
62
+ if (Number.isNaN(parsed) || parsed <= 0) {
63
+ logMessage(mcpServer, "warning", `Ignoring invalid URL_READ_MAX_CHARS="${rawValue}". Expected a positive integer.`);
64
+ return undefined;
65
+ }
66
+ return parsed;
67
+ }
44
68
  /**
45
69
  * Creates and configures a new McpServer with all handlers registered.
46
70
  * Called once per HTTP session, or once for STDIO mode.
@@ -57,11 +81,14 @@ export function createMcpServer() {
57
81
  },
58
82
  });
59
83
  const server = mcpServer.server;
84
+ const useLiteTools = process.env.SEARXNG_LITE_TOOLS === "true";
85
+ const searchTool = useLiteTools ? LITE_WEB_SEARCH_TOOL : WEB_SEARCH_TOOL;
86
+ const readUrlTool = useLiteTools ? LITE_READ_URL_TOOL : READ_URL_TOOL;
60
87
  // List tools handler
61
88
  server.setRequestHandler(ListToolsRequestSchema, async () => {
62
89
  logMessage(mcpServer, "debug", "Handling list_tools request");
63
90
  return {
64
- tools: [WEB_SEARCH_TOOL, READ_URL_TOOL],
91
+ tools: [searchTool, readUrlTool],
65
92
  };
66
93
  });
67
94
  // Call tool handler
@@ -73,7 +100,7 @@ export function createMcpServer() {
73
100
  if (!isSearXNGWebSearchArgs(args)) {
74
101
  throw new Error("Invalid arguments for web search");
75
102
  }
76
- const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch, args.min_score);
103
+ const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch, args.min_score, args.num_results, args.categories);
77
104
  return {
78
105
  content: [
79
106
  {
@@ -87,14 +114,15 @@ export function createMcpServer() {
87
114
  if (!isWebUrlReadArgs(args)) {
88
115
  throw new Error("Invalid arguments for URL reading");
89
116
  }
117
+ const defaultMaxLength = getDefaultUrlReadMaxChars(mcpServer);
90
118
  const paginationOptions = {
91
119
  startChar: args.startChar,
92
- maxLength: args.maxLength,
120
+ maxLength: args.maxLength ?? defaultMaxLength,
93
121
  section: args.section,
94
122
  paragraphRange: args.paragraphRange,
95
123
  readHeadings: args.readHeadings,
96
124
  };
97
- const result = await fetchAndConvertToMarkdown(mcpServer, args.url, 10000, paginationOptions);
125
+ const result = await fetchAndConvertToMarkdown(mcpServer, args.url, getFetchTimeoutMs(mcpServer), paginationOptions);
98
126
  return {
99
127
  content: [
100
128
  {
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): 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): Promise<string>;
package/dist/search.js CHANGED
@@ -1,14 +1,69 @@
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, min_score) {
4
+ function getOperatorMaxResults(mcpServer) {
5
+ const rawValue = process.env.SEARXNG_MAX_RESULTS;
6
+ if (rawValue === undefined || rawValue.trim() === "") {
7
+ return undefined;
8
+ }
9
+ const parsed = parseInt(rawValue, 10);
10
+ if (Number.isNaN(parsed) || parsed <= 0 || parsed > 20) {
11
+ logMessage(mcpServer, "warning", `Ignoring invalid SEARXNG_MAX_RESULTS="${rawValue}". Expected an integer from 1 to 20.`);
12
+ return undefined;
13
+ }
14
+ return parsed;
15
+ }
16
+ function getMaxResultChars(mcpServer) {
17
+ const rawValue = process.env.SEARXNG_MAX_RESULT_CHARS;
18
+ if (rawValue === undefined || rawValue.trim() === "") {
19
+ return undefined;
20
+ }
21
+ const parsed = parseInt(rawValue, 10);
22
+ if (Number.isNaN(parsed) || parsed <= 0) {
23
+ logMessage(mcpServer, "warning", `Ignoring invalid SEARXNG_MAX_RESULT_CHARS="${rawValue}". Expected a positive integer.`);
24
+ return undefined;
25
+ }
26
+ return parsed;
27
+ }
28
+ function truncateResultContent(content, maxResultChars) {
29
+ if (maxResultChars === undefined || content.length <= maxResultChars) {
30
+ return content;
31
+ }
32
+ return `${content.slice(0, maxResultChars)}…`;
33
+ }
34
+ function getDefaultLanguage() {
35
+ return process.env.SEARXNG_DEFAULT_LANGUAGE ?? "all";
36
+ }
37
+ function getDefaultSafesearch(mcpServer) {
38
+ const rawValue = process.env.SEARXNG_DEFAULT_SAFESEARCH;
39
+ if (rawValue === undefined || rawValue.trim() === "") {
40
+ return undefined;
41
+ }
42
+ const parsed = parseInt(rawValue, 10);
43
+ if (Number.isNaN(parsed) || ![0, 1, 2].includes(parsed)) {
44
+ logMessage(mcpServer, "warning", `Ignoring invalid SEARXNG_DEFAULT_SAFESEARCH="${rawValue}". Expected 0, 1, or 2.`);
45
+ return undefined;
46
+ }
47
+ return parsed;
48
+ }
49
+ export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories) {
5
50
  const startTime = Date.now();
51
+ const operatorMax = getOperatorMaxResults(mcpServer);
52
+ const effectiveMax = operatorMax !== undefined
53
+ ? (num_results !== undefined ? Math.min(num_results, operatorMax) : operatorMax)
54
+ : num_results;
55
+ const maxResultChars = getMaxResultChars(mcpServer);
56
+ const effectiveLanguage = language ?? getDefaultLanguage();
57
+ const effectiveSafesearch = safesearch !== undefined ? safesearch : getDefaultSafesearch(mcpServer);
6
58
  // Build detailed log message with all parameters
7
59
  const searchParams = [
8
60
  `page ${pageno}`,
9
- `lang: ${language}`,
61
+ `lang: ${effectiveLanguage}`,
10
62
  time_range ? `time: ${time_range}` : null,
11
- safesearch ? `safesearch: ${safesearch}` : null
63
+ effectiveSafesearch !== undefined ? `safesearch: ${effectiveSafesearch}` : null,
64
+ min_score !== undefined ? `min_score: ${min_score}` : null,
65
+ effectiveMax !== undefined ? `num_results: ${effectiveMax}` : null,
66
+ categories ? `categories: ${categories}` : null,
12
67
  ].filter(Boolean).join(", ");
13
68
  logMessage(mcpServer, "info", `Starting web search: "${query}" (${searchParams})`);
14
69
  const validationError = validateEnvironment();
@@ -26,11 +81,14 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
26
81
  ["day", "week", "month", "year"].includes(time_range)) {
27
82
  url.searchParams.set("time_range", time_range);
28
83
  }
29
- if (language && language !== "all") {
30
- url.searchParams.set("language", language);
84
+ if (effectiveLanguage && effectiveLanguage !== "all") {
85
+ url.searchParams.set("language", effectiveLanguage);
86
+ }
87
+ if (effectiveSafesearch !== undefined && [0, 1, 2].includes(effectiveSafesearch)) {
88
+ url.searchParams.set("safesearch", effectiveSafesearch.toString());
31
89
  }
32
- if (safesearch !== undefined && [0, 1, 2].includes(safesearch)) {
33
- url.searchParams.set("safesearch", safesearch.toString());
90
+ if (categories) {
91
+ url.searchParams.set("categories", categories);
34
92
  }
35
93
  // Prepare request options with headers
36
94
  const requestOptions = {
@@ -126,14 +184,21 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
126
184
  score: result.score || 0,
127
185
  }))
128
186
  .filter((result) => min_score === undefined || result.score >= min_score);
129
- if (results.length === 0) {
130
- const filterNote = min_score === undefined ? "" : ` after applying min_score=${min_score}`;
187
+ const slicedResults = effectiveMax !== undefined
188
+ ? results.slice(0, effectiveMax)
189
+ : results;
190
+ if (slicedResults.length === 0) {
191
+ const appliedFilters = [
192
+ min_score === undefined ? null : `min_score=${min_score}`,
193
+ effectiveMax === undefined ? null : `num_results=${effectiveMax}`,
194
+ ].filter(Boolean).join(" ");
195
+ const filterNote = appliedFilters ? ` after applying ${appliedFilters}` : "";
131
196
  logMessage(mcpServer, "info", `No results found for query: "${query}"${filterNote}`);
132
197
  return createNoResultsMessage(query);
133
198
  }
134
199
  const duration = Date.now() - startTime;
135
- logMessage(mcpServer, "info", `Search completed: "${query}" (${searchParams}) - ${results.length} results in ${duration}ms`);
136
- return results
137
- .map((r) => `Title: ${r.title}\nDescription: ${r.content}\nURL: ${r.url}\nRelevance Score: ${r.score.toFixed(3)}`)
200
+ logMessage(mcpServer, "info", `Search completed: "${query}" (${searchParams}) - ${slicedResults.length} results in ${duration}ms`);
201
+ return slicedResults
202
+ .map((r) => `Title: ${r.title}\nDescription: ${truncateResultContent(r.content, maxResultChars)}\nURL: ${r.url}\nRelevance Score: ${r.score.toFixed(3)}`)
138
203
  .join("\n\n");
139
204
  }
package/dist/types.d.ts CHANGED
@@ -36,6 +36,10 @@ export declare function isSearXNGWebSearchArgs(args: unknown): args is {
36
36
  language?: string;
37
37
  safesearch?: number;
38
38
  min_score?: number;
39
+ num_results?: number;
40
+ categories?: string;
39
41
  };
40
42
  export declare const WEB_SEARCH_TOOL: Tool;
43
+ export declare const LITE_WEB_SEARCH_TOOL: Tool;
44
+ export declare const LITE_READ_URL_TOOL: Tool;
41
45
  export declare const READ_URL_TOOL: Tool;
package/dist/types.js CHANGED
@@ -29,6 +29,17 @@ export function isSearXNGWebSearchArgs(args) {
29
29
  searchArgs.min_score > 1)) {
30
30
  return false;
31
31
  }
32
+ if (searchArgs.num_results !== undefined &&
33
+ (typeof searchArgs.num_results !== "number" ||
34
+ Number.isNaN(searchArgs.num_results) ||
35
+ !Number.isInteger(searchArgs.num_results) ||
36
+ searchArgs.num_results < 1 ||
37
+ searchArgs.num_results > 20)) {
38
+ return false;
39
+ }
40
+ if (searchArgs.categories !== undefined && typeof searchArgs.categories !== "string") {
41
+ return false;
42
+ }
32
43
  return true;
33
44
  }
34
45
  export const WEB_SEARCH_TOOL = {
@@ -76,10 +87,38 @@ export const WEB_SEARCH_TOOL = {
76
87
  minimum: 0,
77
88
  maximum: 1,
78
89
  },
90
+ num_results: {
91
+ type: "number",
92
+ description: "Maximum number of results to return (1-20). Operator cap SEARXNG_MAX_RESULTS applies as a ceiling.",
93
+ minimum: 1,
94
+ maximum: 20,
95
+ },
96
+ categories: {
97
+ type: "string",
98
+ description: "Comma-separated SearXNG categories. Supported: general, news, images, videos, it, science, files, social media. Default: general (SearXNG instance default).",
99
+ },
79
100
  },
80
101
  required: ["query"],
81
102
  },
82
103
  };
104
+ export const LITE_WEB_SEARCH_TOOL = {
105
+ name: "searxng_web_search",
106
+ description: "Web search. Returns titles, URLs, snippets.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: { query: { type: "string", description: "Search query." } },
110
+ required: ["query"],
111
+ },
112
+ };
113
+ export const LITE_READ_URL_TOOL = {
114
+ name: "web_url_read",
115
+ description: "Fetch URL. Returns page text as markdown.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: { url: { type: "string", description: "URL to fetch." } },
119
+ required: ["url"],
120
+ },
121
+ };
83
122
  export const READ_URL_TOOL = {
84
123
  name: "web_url_read",
85
124
  description: "Fetches a URL and returns its text content converted to markdown. " +
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type Dispatcher } from "undici";
2
3
  interface PaginationOptions {
3
4
  startChar?: number;
4
5
  maxLength?: number;
@@ -6,5 +7,7 @@ interface PaginationOptions {
6
7
  paragraphRange?: string;
7
8
  readHeadings?: boolean;
8
9
  }
10
+ export declare const DEFAULT_MAX_CONTENT_LENGTH_BYTES: number;
11
+ export declare function checkContentLength(mcpServer: McpServer, url: string, timeoutMs: number, dispatcher?: Dispatcher, baseRequestOptions?: RequestInit): Promise<number | null>;
9
12
  export declare function fetchAndConvertToMarkdown(mcpServer: McpServer, url: string, timeoutMs?: number, paginationOptions?: PaginationOptions): Promise<string>;
10
13
  export {};
@@ -8,6 +8,8 @@ import { getHttpSecurityConfig } from "./http-security.js";
8
8
  import { createURLFormatError, createURLSecurityPolicyError, createNetworkError, createServerError, createContentError, createConversionError, createTimeoutError, createEmptyContentWarning, createUnexpectedError } from "./error-handler.js";
9
9
  const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
10
10
  const MAX_REDIRECTS = 5;
11
+ export const DEFAULT_MAX_CONTENT_LENGTH_BYTES = 5 * 1024 * 1024;
12
+ const HEAD_TIMEOUT_CAP_MS = 3000;
11
13
  function isPrivateHostname(hostname) {
12
14
  const lower = hostname.toLowerCase().replace(/\.+$/, "");
13
15
  return lower === "localhost" || lower.endsWith(".localhost");
@@ -162,6 +164,53 @@ function applyPaginationOptions(markdownContent, options) {
162
164
  }
163
165
  return result;
164
166
  }
167
+ export async function checkContentLength(mcpServer, url, timeoutMs, dispatcher, baseRequestOptions = {}) {
168
+ const controller = new AbortController();
169
+ const timeoutId = setTimeout(() => controller.abort(), Math.min(timeoutMs, HEAD_TIMEOUT_CAP_MS));
170
+ try {
171
+ const requestOptions = {
172
+ ...baseRequestOptions,
173
+ method: "HEAD",
174
+ signal: controller.signal,
175
+ redirect: "manual",
176
+ };
177
+ if (dispatcher) {
178
+ requestOptions.dispatcher = dispatcher;
179
+ }
180
+ const response = await undiciFetch(url, requestOptions);
181
+ const contentLength = response.headers.get("content-length");
182
+ if (!contentLength) {
183
+ return null;
184
+ }
185
+ const parsed = parseInt(contentLength, 10);
186
+ return Number.isNaN(parsed) || parsed < 0 ? null : parsed;
187
+ }
188
+ catch (error) {
189
+ logMessage(mcpServer, "warning", `HEAD check failed (proceeding with GET): ${error.message}`);
190
+ return null;
191
+ }
192
+ finally {
193
+ clearTimeout(timeoutId);
194
+ }
195
+ }
196
+ function getMaxContentLengthBytes(mcpServer) {
197
+ const rawValue = process.env.URL_READ_MAX_CONTENT_LENGTH_BYTES;
198
+ if (rawValue === undefined || rawValue.trim() === "") {
199
+ return DEFAULT_MAX_CONTENT_LENGTH_BYTES;
200
+ }
201
+ const parsed = parseInt(rawValue, 10);
202
+ if (Number.isNaN(parsed) || parsed <= 0) {
203
+ logMessage(mcpServer, "warning", `Ignoring invalid URL_READ_MAX_CONTENT_LENGTH_BYTES="${rawValue}". Expected a positive integer; using default ${DEFAULT_MAX_CONTENT_LENGTH_BYTES}.`);
204
+ return DEFAULT_MAX_CONTENT_LENGTH_BYTES;
205
+ }
206
+ return parsed;
207
+ }
208
+ function createContentTooLargeMessage(contentLength, maxBytes) {
209
+ const sizeMB = (contentLength / (1024 * 1024)).toFixed(1);
210
+ const limitMB = (maxBytes / (1024 * 1024)).toFixed(1);
211
+ return (`Content too large: server reports Content-Length of ${sizeMB} MB (limit: ${limitMB} MB). ` +
212
+ `Try using readHeadings or section to fetch only the relevant parts.`);
213
+ }
165
214
  export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 10000, paginationOptions = {}) {
166
215
  const startTime = Date.now();
167
216
  logMessage(mcpServer, "info", `Fetching URL: ${url}`);
@@ -184,6 +233,7 @@ export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 1000
184
233
  throw createURLFormatError(url);
185
234
  }
186
235
  assertUrlAllowed(parsedUrl);
236
+ const maxContentLengthBytes = getMaxContentLengthBytes(mcpServer);
187
237
  // Create an AbortController instance
188
238
  const controller = new AbortController();
189
239
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -216,6 +266,10 @@ export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 1000
216
266
  if (dispatcher) {
217
267
  currentRequestOptions.dispatcher = dispatcher;
218
268
  }
269
+ const contentLength = await checkContentLength(mcpServer, currentUrl.toString(), timeoutMs, dispatcher, currentRequestOptions);
270
+ if (contentLength !== null && contentLength > maxContentLengthBytes) {
271
+ return createContentTooLargeMessage(contentLength, maxContentLengthBytes);
272
+ }
219
273
  // Fetch the URL with the abort signal.
220
274
  // Use undici's own fetch so it shares the same internal version as the
221
275
  // Agent/ProxyAgent dispatcher — avoids the Node.js bundled-undici vs
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const packageVersion = "1.3.3";
1
+ export declare const packageVersion = "1.4.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "1.3.3";
1
+ export const packageVersion = "1.4.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-searxng",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
5
5
  "description": "MCP server for SearXNG integration",
6
6
  "license": "MIT",
@@ -66,6 +66,7 @@
66
66
  "cross-env": "^10.1.0",
67
67
  "eslint": "10.4.1",
68
68
  "eslint-plugin-security": "^4.0.0",
69
+ "fast-check": "^4.8.0",
69
70
  "shx": "^0.4.0",
70
71
  "supertest": "^7.2.2",
71
72
  "tsx": "4.22.4",