mcp-searxng 0.9.2 → 0.10.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 CHANGED
@@ -49,12 +49,29 @@ An [MCP server](https://modelcontextprotocol.io/introduction) implementation tha
49
49
  - Example: `https://search.example.com`
50
50
 
51
51
  #### Optional
52
- - **`AUTH_USERNAME`** / **`AUTH_PASSWORD`**: HTTP Basic Auth credentials for password-protected instances
53
- - **`USER_AGENT`**: Custom User-Agent header (e.g., `MyBot/1.0`)
54
- - **`HTTP_PROXY`** / **`HTTPS_PROXY`**: Proxy URLs for routing traffic
52
+ - **`AUTH_USERNAME`** / **`AUTH_PASSWORD`**: HTTP Basic Auth credentials for `searxng_web_search` (password-protected SearXNG instances)
53
+ - **`USER_AGENT`**: Global default User-Agent header used by both `searxng_web_search` and `web_url_read` (e.g., `MyBot/1.0`)
54
+ - **`URL_READER_USER_AGENT`**: Custom User-Agent specifically for the `web_url_read` tool (overrides `USER_AGENT` for URL reading requests)
55
+ - **`HTTP_PROXY`** / **`HTTPS_PROXY`**: Global proxy URLs for routing traffic (fallback for both interfaces)
55
56
  - Format: `http://[username:password@]proxy.host:port`
56
57
  - **`NO_PROXY`**: Comma-separated bypass list (e.g., `localhost,.internal,example.com`)
57
58
 
59
+ ##### Interface-Specific Proxies (Optional)
60
+ - **`SEARCH_HTTP_PROXY`** / **`SEARCH_HTTPS_PROXY`**: Proxy for `searxng_web_search` tool only
61
+ - **`URL_READER_HTTP_PROXY`** / **`URL_READER_HTTPS_PROXY`**: Proxy for `web_url_read` tool only
62
+ - These take priority over `HTTP_PROXY`/`HTTPS_PROXY` for their respective interfaces
63
+
64
+ #### Advanced Configuration
65
+
66
+ ```bash
67
+ # Separate proxies for search and URL reading
68
+ SEARCH_HTTP_PROXY=http://search-proxy:8080
69
+ URL_READER_HTTP_PROXY=http://reader-proxy:8080
70
+
71
+ # Custom user_agent for URL reader
72
+ URL_READER_USER_AGENT="Mozilla/5.0 (compatible; Bot/1.0)"
73
+ ```
74
+
58
75
  ## Installation & Configuration
59
76
 
60
77
  ### [NPX](https://www.npmjs.com/package/mcp-searxng)
@@ -87,8 +104,11 @@ An [MCP server](https://modelcontextprotocol.io/introduction) implementation tha
87
104
  "AUTH_USERNAME": "your_username",
88
105
  "AUTH_PASSWORD": "your_password",
89
106
  "USER_AGENT": "MyBot/1.0",
90
- "HTTP_PROXY": "http://proxy.company.com:8080",
91
- "HTTPS_PROXY": "http://proxy.company.com:8080",
107
+ "URL_READER_USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
108
+ "SEARCH_HTTP_PROXY": "http://search-proxy.company.com:8080",
109
+ "URL_READER_HTTP_PROXY": "http://reader-proxy.company.com:8080",
110
+ "HTTP_PROXY": "http://global-proxy.company.com:8080",
111
+ "HTTPS_PROXY": "http://global-proxy.company.com:8080",
92
112
  "NO_PROXY": "localhost,127.0.0.1,.local,.internal"
93
113
  }
94
114
  }
@@ -132,15 +152,17 @@ npm install -g mcp-searxng
132
152
  "AUTH_USERNAME": "your_username",
133
153
  "AUTH_PASSWORD": "your_password",
134
154
  "USER_AGENT": "MyBot/1.0",
135
- "HTTP_PROXY": "http://proxy.company.com:8080",
136
- "HTTPS_PROXY": "http://proxy.company.com:8080",
155
+ "URL_READER_USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
156
+ "SEARCH_HTTP_PROXY": "http://search-proxy.company.com:8080",
157
+ "URL_READER_HTTP_PROXY": "http://reader-proxy.company.com:8080",
158
+ "HTTP_PROXY": "http://global-proxy.company.com:8080",
159
+ "HTTPS_PROXY": "http://global-proxy.company.com:8080",
137
160
  "NO_PROXY": "localhost,127.0.0.1,.local,.internal"
138
161
  }
139
162
  }
140
163
  }
141
164
  }
142
165
  ```
143
-
144
166
  </details>
145
167
 
146
168
  ### Docker
@@ -183,6 +205,11 @@ docker pull isokoliuk/mcp-searxng:latest
183
205
  "-e", "AUTH_USERNAME",
184
206
  "-e", "AUTH_PASSWORD",
185
207
  "-e", "USER_AGENT",
208
+ "-e", "URL_READER_USER_AGENT",
209
+ "-e", "SEARCH_HTTP_PROXY",
210
+ "-e", "SEARCH_HTTPS_PROXY",
211
+ "-e", "URL_READER_HTTP_PROXY",
212
+ "-e", "URL_READER_HTTPS_PROXY",
186
213
  "-e", "HTTP_PROXY",
187
214
  "-e", "HTTPS_PROXY",
188
215
  "-e", "NO_PROXY",
@@ -193,8 +220,11 @@ docker pull isokoliuk/mcp-searxng:latest
193
220
  "AUTH_USERNAME": "your_username",
194
221
  "AUTH_PASSWORD": "your_password",
195
222
  "USER_AGENT": "MyBot/1.0",
196
- "HTTP_PROXY": "http://proxy.company.com:8080",
197
- "HTTPS_PROXY": "http://proxy.company.com:8080",
223
+ "URL_READER_USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
224
+ "SEARCH_HTTP_PROXY": "http://search-proxy.company.com:8080",
225
+ "URL_READER_HTTP_PROXY": "http://reader-proxy.company.com:8080",
226
+ "HTTP_PROXY": "http://global-proxy.company.com:8080",
227
+ "HTTPS_PROXY": "http://global-proxy.company.com:8080",
198
228
  "NO_PROXY": "localhost,127.0.0.1,.local,.internal"
199
229
  }
200
230
  }
@@ -229,7 +259,10 @@ services:
229
259
  # - AUTH_USERNAME=your_username
230
260
  # - AUTH_PASSWORD=your_password
231
261
  # - USER_AGENT=MyBot/1.0
232
- # - HTTP_PROXY=http://proxy.company.com:8080
262
+ # - URL_READER_USER_AGENT=Mozilla/5.0 (compatible; MyBot/1.0)
263
+ # - SEARCH_HTTP_PROXY=http://search-proxy.company.com:8080
264
+ # - URL_READER_HTTP_PROXY=http://reader-proxy.company.com:8080
265
+ # - HTTP_PROXY=http://global-proxy.company.com:8080
233
266
  # - HTTPS_PROXY=http://proxy.company.com:8080
234
267
  # - NO_PROXY=localhost,127.0.0.1,.local,.internal
235
268
  ```
@@ -275,6 +308,40 @@ MCP_HTTP_PORT=3000 SEARXNG_URL=http://localhost:8080 mcp-searxng
275
308
  curl http://localhost:3000/health
276
309
  ```
277
310
 
311
+ ## Troubleshooting
312
+
313
+ ### 403 Forbidden Error from SearXNG
314
+
315
+ If you receive a `403 Forbidden` error when using `mcp-searxng`, it is likely because your SearXNG instance does not have JSON format enabled. This server requests results in JSON format (`format=json`), which must be explicitly allowed in SearXNG's configuration.
316
+
317
+ **To fix this**, edit your SearXNG `settings.yml` (commonly located at `/etc/searxng/settings.yml`) and add `json` to the list of allowed formats:
318
+
319
+ ```yaml
320
+ search:
321
+ formats:
322
+ - html
323
+ - json
324
+ ```
325
+
326
+ After saving the file, restart your SearXNG instance. For example, if running with Docker:
327
+
328
+ ```bash
329
+ docker restart searxng
330
+ ```
331
+
332
+ You can verify JSON format is working by running:
333
+
334
+ ```bash
335
+ curl 'http://localhost:8080/search?q=test&format=json'
336
+ ```
337
+
338
+ You should receive a JSON response. If you still get a 403 error, double-check that:
339
+ - The `settings.yml` file is correctly mounted into your Docker container
340
+ - The YAML indentation is correct
341
+ - The SearXNG instance was fully restarted after the configuration change
342
+
343
+ For more details, see the [SearXNG settings documentation](https://docs.searxng.org/admin/settings/settings.html) and [this discussion](https://github.com/searxng/searxng/discussions/1789).
344
+
278
345
  ## Running evals
279
346
 
280
347
  ```bash
@@ -1,3 +1,3 @@
1
1
  import express from "express";
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- export declare function createHttpServer(server: Server): Promise<express.Application>;
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ export declare function createHttpServer(mcpServer: McpServer): Promise<express.Application>;
@@ -5,7 +5,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
5
5
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { logMessage } from "./logging.js";
7
7
  import { packageVersion } from "./index.js";
8
- export async function createHttpServer(server) {
8
+ export async function createHttpServer(mcpServer) {
9
9
  const app = express();
10
10
  app.use(express.json());
11
11
  // Add CORS support for web clients
@@ -23,16 +23,16 @@ export async function createHttpServer(server) {
23
23
  if (sessionId && transports[sessionId]) {
24
24
  // Reuse existing transport
25
25
  transport = transports[sessionId];
26
- logMessage(server, "debug", `Reusing session: ${sessionId}`);
26
+ logMessage(mcpServer, "debug", `Reusing session: ${sessionId}`);
27
27
  }
28
28
  else if (!sessionId && isInitializeRequest(req.body)) {
29
29
  // New initialization request
30
- logMessage(server, "info", "Creating new HTTP session");
30
+ logMessage(mcpServer, "info", "Creating new HTTP session");
31
31
  transport = new StreamableHTTPServerTransport({
32
32
  sessionIdGenerator: () => randomUUID(),
33
33
  onsessioninitialized: (sessionId) => {
34
34
  transports[sessionId] = transport;
35
- logMessage(server, "debug", `Session initialized: ${sessionId}`);
35
+ logMessage(mcpServer, "debug", `Session initialized: ${sessionId}`);
36
36
  },
37
37
  // DNS rebinding protection disabled by default for backwards compatibility
38
38
  // For production, consider enabling:
@@ -42,17 +42,17 @@ export async function createHttpServer(server) {
42
42
  // Clean up transport when closed
43
43
  transport.onclose = () => {
44
44
  if (transport.sessionId) {
45
- logMessage(server, "debug", `Session closed: ${transport.sessionId}`);
45
+ logMessage(mcpServer, "debug", `Session closed: ${transport.sessionId}`);
46
46
  delete transports[transport.sessionId];
47
47
  }
48
48
  };
49
49
  // Connect the existing server to the new transport
50
- await server.connect(transport);
50
+ await mcpServer.connect(transport);
51
51
  }
52
52
  else {
53
53
  // Invalid request
54
54
  console.warn(`⚠️ POST request rejected - invalid request:`, {
55
- clientIP: req.ip || req.connection.remoteAddress,
55
+ clientIP: req.ip || req.socket.remoteAddress,
56
56
  sessionId: sessionId || 'undefined',
57
57
  hasInitializeRequest: isInitializeRequest(req.body),
58
58
  userAgent: req.headers['user-agent'],
@@ -77,7 +77,7 @@ export async function createHttpServer(server) {
77
77
  // Log header-related rejections for debugging
78
78
  if (error instanceof Error && error.message.includes('accept')) {
79
79
  console.warn(`⚠️ Connection rejected due to missing headers:`, {
80
- clientIP: req.ip || req.connection.remoteAddress,
80
+ clientIP: req.ip || req.socket.remoteAddress,
81
81
  userAgent: req.headers['user-agent'],
82
82
  contentType: req.headers['content-type'],
83
83
  accept: req.headers['accept'],
@@ -92,7 +92,7 @@ export async function createHttpServer(server) {
92
92
  const sessionId = req.headers['mcp-session-id'];
93
93
  if (!sessionId || !transports[sessionId]) {
94
94
  console.warn(`⚠️ GET request rejected - missing or invalid session ID:`, {
95
- clientIP: req.ip || req.connection.remoteAddress,
95
+ clientIP: req.ip || req.socket.remoteAddress,
96
96
  sessionId: sessionId || 'undefined',
97
97
  userAgent: req.headers['user-agent']
98
98
  });
@@ -105,7 +105,7 @@ export async function createHttpServer(server) {
105
105
  }
106
106
  catch (error) {
107
107
  console.warn(`⚠️ GET request failed:`, {
108
- clientIP: req.ip || req.connection.remoteAddress,
108
+ clientIP: req.ip || req.socket.remoteAddress,
109
109
  sessionId,
110
110
  error: error instanceof Error ? error.message : String(error)
111
111
  });
@@ -117,7 +117,7 @@ export async function createHttpServer(server) {
117
117
  const sessionId = req.headers['mcp-session-id'];
118
118
  if (!sessionId || !transports[sessionId]) {
119
119
  console.warn(`⚠️ DELETE request rejected - missing or invalid session ID:`, {
120
- clientIP: req.ip || req.connection.remoteAddress,
120
+ clientIP: req.ip || req.socket.remoteAddress,
121
121
  sessionId: sessionId || 'undefined',
122
122
  userAgent: req.headers['user-agent']
123
123
  });
@@ -130,7 +130,7 @@ export async function createHttpServer(server) {
130
130
  }
131
131
  catch (error) {
132
132
  console.warn(`⚠️ DELETE request failed:`, {
133
- clientIP: req.ip || req.connection.remoteAddress,
133
+ clientIP: req.ip || req.socket.remoteAddress,
134
134
  sessionId,
135
135
  error: error instanceof Error ? error.message : String(error)
136
136
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- declare const packageVersion = "0.9.2";
2
+ declare const packageVersion = "0.10.1";
3
3
  export { packageVersion };
4
4
  export declare function isWebUrlReadArgs(args: unknown): args is {
5
5
  url: string;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  // Import modularized functionality
@@ -11,7 +11,7 @@ import { createConfigResource, createHelpResource } from "./resources.js";
11
11
  import { createHttpServer } from "./http-server.js";
12
12
  import { validateEnvironment as validateEnv } from "./error-handler.js";
13
13
  // Use a static version string that will be updated by the version script
14
- const packageVersion = "0.9.2";
14
+ const packageVersion = "0.10.1";
15
15
  // Export the version for use in other modules
16
16
  export { packageVersion };
17
17
  // Global state for logging level
@@ -49,28 +49,21 @@ export function isWebUrlReadArgs(args) {
49
49
  return true;
50
50
  }
51
51
  // Server implementation
52
- const server = new Server({
52
+ const mcpServer = new McpServer({
53
53
  name: "ihor-sokoliuk/mcp-searxng",
54
54
  version: packageVersion,
55
55
  }, {
56
56
  capabilities: {
57
57
  logging: {},
58
58
  resources: {},
59
- tools: {
60
- searxng_web_search: {
61
- description: WEB_SEARCH_TOOL.description,
62
- schema: WEB_SEARCH_TOOL.inputSchema,
63
- },
64
- web_url_read: {
65
- description: READ_URL_TOOL.description,
66
- schema: READ_URL_TOOL.inputSchema,
67
- },
68
- },
59
+ tools: {},
69
60
  },
70
61
  });
62
+ // Underlying low-level server for handler registration and passing to modules
63
+ const server = mcpServer.server;
71
64
  // List tools handler
72
65
  server.setRequestHandler(ListToolsRequestSchema, async () => {
73
- logMessage(server, "debug", "Handling list_tools request");
66
+ logMessage(mcpServer, "debug", "Handling list_tools request");
74
67
  return {
75
68
  tools: [WEB_SEARCH_TOOL, READ_URL_TOOL],
76
69
  };
@@ -78,13 +71,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
78
71
  // Call tool handler
79
72
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
80
73
  const { name, arguments: args } = request.params;
81
- logMessage(server, "debug", `Handling call_tool request: ${name}`);
74
+ logMessage(mcpServer, "debug", `Handling call_tool request: ${name}`);
82
75
  try {
83
76
  if (name === "searxng_web_search") {
84
77
  if (!isSearXNGWebSearchArgs(args)) {
85
78
  throw new Error("Invalid arguments for web search");
86
79
  }
87
- const result = await performWebSearch(server, args.query, args.pageno, args.time_range, args.language, args.safesearch);
80
+ const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch);
88
81
  return {
89
82
  content: [
90
83
  {
@@ -105,7 +98,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
105
98
  paragraphRange: args.paragraphRange,
106
99
  readHeadings: args.readHeadings,
107
100
  };
108
- const result = await fetchAndConvertToMarkdown(server, args.url, 10000, paginationOptions);
101
+ const result = await fetchAndConvertToMarkdown(mcpServer, args.url, 10000, paginationOptions);
109
102
  return {
110
103
  content: [
111
104
  {
@@ -120,7 +113,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
120
113
  }
121
114
  }
122
115
  catch (error) {
123
- logMessage(server, "error", `Tool execution error: ${error instanceof Error ? error.message : String(error)}`, {
116
+ logMessage(mcpServer, "error", `Tool execution error: ${error instanceof Error ? error.message : String(error)}`, {
124
117
  tool: name,
125
118
  args: args,
126
119
  error: error instanceof Error ? error.stack : String(error)
@@ -131,14 +124,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
131
124
  // Logging level handler
132
125
  server.setRequestHandler(SetLevelRequestSchema, async (request) => {
133
126
  const { level } = request.params;
134
- logMessage(server, "info", `Setting log level to: ${level}`);
127
+ logMessage(mcpServer, "info", `Setting log level to: ${level}`);
135
128
  currentLogLevel = level;
136
129
  setLogLevel(level);
137
130
  return {};
138
131
  });
139
132
  // List resources handler
140
133
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
141
- logMessage(server, "debug", "Handling list_resources request");
134
+ logMessage(mcpServer, "debug", "Handling list_resources request");
142
135
  return {
143
136
  resources: [
144
137
  {
@@ -158,13 +151,13 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
158
151
  });
159
152
  // List resource templates handler
160
153
  server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
161
- logMessage(server, "debug", "Handling list_resource_templates request");
154
+ logMessage(mcpServer, "debug", "Handling list_resource_templates request");
162
155
  return { resourceTemplates: [] };
163
156
  });
164
157
  // Read resource handler
165
158
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
166
159
  const { uri } = request.params;
167
- logMessage(server, "debug", `Handling read_resource request for: ${uri}`);
160
+ logMessage(mcpServer, "debug", `Handling read_resource request for: ${uri}`);
168
161
  switch (uri) {
169
162
  case "config://server-config":
170
163
  return {
@@ -207,7 +200,7 @@ async function main() {
207
200
  process.exit(1);
208
201
  }
209
202
  console.log(`Starting HTTP transport on port ${port}`);
210
- const app = await createHttpServer(server);
203
+ const app = await createHttpServer(mcpServer);
211
204
  const httpServer = app.listen(port, () => {
212
205
  console.log(`HTTP server listening on port ${port}`);
213
206
  console.log(`Health check: http://localhost:${port}/health`);
@@ -234,12 +227,12 @@ async function main() {
234
227
  console.error("📡 Waiting for MCP client connection via STDIO...\n");
235
228
  }
236
229
  const transport = new StdioServerTransport();
237
- await server.connect(transport);
230
+ await mcpServer.connect(transport);
238
231
  // Log after connection is established
239
- logMessage(server, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
240
- logMessage(server, "info", `Log level: ${currentLogLevel}`);
241
- logMessage(server, "info", `Environment: ${process.env.NODE_ENV || 'development'}`);
242
- logMessage(server, "info", `SearXNG URL: ${process.env.SEARXNG_URL || 'not configured'}`);
232
+ logMessage(mcpServer, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
233
+ logMessage(mcpServer, "info", `Log level: ${currentLogLevel}`);
234
+ logMessage(mcpServer, "info", `Environment: ${process.env.NODE_ENV || 'development'}`);
235
+ logMessage(mcpServer, "info", `SearXNG URL: ${process.env.SEARXNG_URL || 'not configured'}`);
243
236
  }
244
237
  }
245
238
  // Handle uncaught errors
package/dist/logging.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
3
- export declare function logMessage(server: Server, level: LoggingLevel, message: string, data?: unknown): void;
3
+ export declare function logMessage(mcpServer: McpServer, level: LoggingLevel, message: string, data?: unknown): void;
4
4
  export declare function shouldLog(level: LoggingLevel): boolean;
5
5
  export declare function setLogLevel(level: LoggingLevel): void;
6
6
  export declare function getCurrentLogLevel(): LoggingLevel;
package/dist/logging.js CHANGED
@@ -1,19 +1,16 @@
1
1
  // Logging state
2
2
  let currentLogLevel = "info";
3
3
  // Logging helper function
4
- export function logMessage(server, level, message, data) {
4
+ export function logMessage(mcpServer, level, message, data) {
5
5
  if (shouldLog(level)) {
6
6
  try {
7
7
  // Merge message and data together for the notification body
8
8
  const notificationData = data !== undefined
9
9
  ? (typeof data === 'object' && data !== null ? { message, ...data } : { message, data })
10
10
  : { message };
11
- server.notification({
12
- method: "notifications/message",
13
- params: {
14
- level,
15
- data: notificationData
16
- }
11
+ mcpServer.sendLoggingMessage({
12
+ level,
13
+ data: notificationData
17
14
  }).catch((error) => {
18
15
  // Silently ignore "Not connected" errors during server startup
19
16
  // This can happen when logging occurs before the transport is fully connected
package/dist/proxy.d.ts CHANGED
@@ -1,16 +1,39 @@
1
1
  import { ProxyAgent } from "undici";
2
+ /**
3
+ * Proxy configuration type for separating search and URL reader proxies.
4
+ */
5
+ export declare const ProxyType: {
6
+ readonly SEARCH: "search";
7
+ readonly URL_READER: "url_reader";
8
+ };
9
+ export type ProxyType = typeof ProxyType[keyof typeof ProxyType];
2
10
  /**
3
11
  * Creates a proxy agent dispatcher for Node.js fetch API.
4
12
  *
5
13
  * Node.js fetch uses Undici under the hood, which requires a 'dispatcher' option
6
14
  * instead of 'agent'. This function creates a ProxyAgent compatible with fetch.
7
15
  *
8
- * Environment variables checked (in order):
9
- * - HTTP_PROXY / http_proxy: For HTTP requests
10
- * - HTTPS_PROXY / https_proxy: For HTTPS requests
16
+ * Environment variables checked (in order, depending on URL protocol):
17
+ * - For type 'search' and HTTPS URLs:
18
+ * SEARCH_HTTPS_PROXY, SEARCH_HTTP_PROXY, search_https_proxy, search_http_proxy,
19
+ * then HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
20
+ * - For type 'search' and HTTP/unknown URLs:
21
+ * SEARCH_HTTP_PROXY, SEARCH_HTTPS_PROXY, search_http_proxy, search_https_proxy,
22
+ * then HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
23
+ * - For type 'url_reader' and HTTPS URLs:
24
+ * URL_READER_HTTPS_PROXY, URL_READER_HTTP_PROXY, url_reader_https_proxy, url_reader_http_proxy,
25
+ * then HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
26
+ * - For type 'url_reader' and HTTP/unknown URLs:
27
+ * URL_READER_HTTP_PROXY, URL_READER_HTTPS_PROXY, url_reader_http_proxy, url_reader_https_proxy,
28
+ * then HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
29
+ * - For no specific type and HTTPS URLs:
30
+ * HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
31
+ * - For no specific type and HTTP/unknown URLs:
32
+ * HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
11
33
  * - NO_PROXY / no_proxy: Comma-separated list of hosts to bypass proxy
12
34
  *
13
35
  * @param targetUrl - Optional target URL to check against NO_PROXY rules
36
+ * @param type - Optional proxy type ('search' or 'url_reader') for separate proxy configs
14
37
  * @returns ProxyAgent dispatcher for fetch, or undefined if no proxy configured or bypassed
15
38
  */
16
- export declare function createProxyAgent(targetUrl?: string): ProxyAgent | undefined;
39
+ export declare function createProxyAgent(targetUrl?: string, type?: ProxyType): ProxyAgent | undefined;
package/dist/proxy.js CHANGED
@@ -50,22 +50,116 @@ function shouldBypassProxy(targetUrl) {
50
50
  }
51
51
  return false;
52
52
  }
53
+ /**
54
+ * Proxy configuration type for separating search and URL reader proxies.
55
+ */
56
+ export const ProxyType = {
57
+ SEARCH: 'search',
58
+ URL_READER: 'url_reader',
59
+ };
60
+ /**
61
+ * Gets proxy URL for the specified proxy type.
62
+ * Checks type-specific proxy first, then falls back to global proxy.
63
+ *
64
+ * @param type - The type of proxy to get ('search' or 'url_reader')
65
+ * @param targetUrl - Optional target URL whose protocol is used to select between HTTP and HTTPS proxies
66
+ * @returns The proxy URL or undefined if not configured
67
+ */
68
+ function getProxyUrl(type, targetUrl) {
69
+ let isHttps = false;
70
+ if (targetUrl) {
71
+ try {
72
+ const url = new URL(targetUrl);
73
+ isHttps = url.protocol === 'https:';
74
+ }
75
+ catch {
76
+ isHttps = false;
77
+ }
78
+ }
79
+ if (type === ProxyType.SEARCH) {
80
+ if (isHttps) {
81
+ return process.env.SEARCH_HTTPS_PROXY ||
82
+ process.env.SEARCH_HTTP_PROXY ||
83
+ process.env.search_https_proxy ||
84
+ process.env.search_http_proxy ||
85
+ process.env.HTTPS_PROXY ||
86
+ process.env.HTTP_PROXY ||
87
+ process.env.https_proxy ||
88
+ process.env.http_proxy;
89
+ }
90
+ return process.env.SEARCH_HTTP_PROXY ||
91
+ process.env.SEARCH_HTTPS_PROXY ||
92
+ process.env.search_http_proxy ||
93
+ process.env.search_https_proxy ||
94
+ // Fallback to global proxies
95
+ process.env.HTTP_PROXY ||
96
+ process.env.HTTPS_PROXY ||
97
+ process.env.http_proxy ||
98
+ process.env.https_proxy;
99
+ }
100
+ if (type === ProxyType.URL_READER) {
101
+ if (isHttps) {
102
+ return process.env.URL_READER_HTTPS_PROXY ||
103
+ process.env.URL_READER_HTTP_PROXY ||
104
+ process.env.url_reader_https_proxy ||
105
+ process.env.url_reader_http_proxy ||
106
+ process.env.HTTPS_PROXY ||
107
+ process.env.HTTP_PROXY ||
108
+ process.env.https_proxy ||
109
+ process.env.http_proxy;
110
+ }
111
+ return process.env.URL_READER_HTTP_PROXY ||
112
+ process.env.URL_READER_HTTPS_PROXY ||
113
+ process.env.url_reader_http_proxy ||
114
+ process.env.url_reader_https_proxy ||
115
+ // Fallback to global proxies
116
+ process.env.HTTP_PROXY ||
117
+ process.env.HTTPS_PROXY ||
118
+ process.env.http_proxy ||
119
+ process.env.https_proxy;
120
+ }
121
+ if (isHttps) {
122
+ return process.env.HTTPS_PROXY ||
123
+ process.env.HTTP_PROXY ||
124
+ process.env.https_proxy ||
125
+ process.env.http_proxy;
126
+ }
127
+ return process.env.HTTP_PROXY ||
128
+ process.env.HTTPS_PROXY ||
129
+ process.env.http_proxy ||
130
+ process.env.https_proxy;
131
+ }
53
132
  /**
54
133
  * Creates a proxy agent dispatcher for Node.js fetch API.
55
134
  *
56
135
  * Node.js fetch uses Undici under the hood, which requires a 'dispatcher' option
57
136
  * instead of 'agent'. This function creates a ProxyAgent compatible with fetch.
58
137
  *
59
- * Environment variables checked (in order):
60
- * - HTTP_PROXY / http_proxy: For HTTP requests
61
- * - HTTPS_PROXY / https_proxy: For HTTPS requests
138
+ * Environment variables checked (in order, depending on URL protocol):
139
+ * - For type 'search' and HTTPS URLs:
140
+ * SEARCH_HTTPS_PROXY, SEARCH_HTTP_PROXY, search_https_proxy, search_http_proxy,
141
+ * then HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
142
+ * - For type 'search' and HTTP/unknown URLs:
143
+ * SEARCH_HTTP_PROXY, SEARCH_HTTPS_PROXY, search_http_proxy, search_https_proxy,
144
+ * then HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
145
+ * - For type 'url_reader' and HTTPS URLs:
146
+ * URL_READER_HTTPS_PROXY, URL_READER_HTTP_PROXY, url_reader_https_proxy, url_reader_http_proxy,
147
+ * then HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
148
+ * - For type 'url_reader' and HTTP/unknown URLs:
149
+ * URL_READER_HTTP_PROXY, URL_READER_HTTPS_PROXY, url_reader_http_proxy, url_reader_https_proxy,
150
+ * then HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
151
+ * - For no specific type and HTTPS URLs:
152
+ * HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
153
+ * - For no specific type and HTTP/unknown URLs:
154
+ * HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
62
155
  * - NO_PROXY / no_proxy: Comma-separated list of hosts to bypass proxy
63
156
  *
64
157
  * @param targetUrl - Optional target URL to check against NO_PROXY rules
158
+ * @param type - Optional proxy type ('search' or 'url_reader') for separate proxy configs
65
159
  * @returns ProxyAgent dispatcher for fetch, or undefined if no proxy configured or bypassed
66
160
  */
67
- export function createProxyAgent(targetUrl) {
68
- const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy;
161
+ export function createProxyAgent(targetUrl, type) {
162
+ const proxyUrl = getProxyUrl(type, targetUrl);
69
163
  if (!proxyUrl) {
70
164
  return undefined;
71
165
  }
package/dist/search.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- export declare function performWebSearch(server: Server, query: string, pageno?: number, time_range?: string, language?: string, safesearch?: number): Promise<string>;
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>;
package/dist/search.js CHANGED
@@ -1,7 +1,7 @@
1
- import { createProxyAgent } from "./proxy.js";
1
+ import { createProxyAgent, ProxyType } from "./proxy.js";
2
2
  import { logMessage } from "./logging.js";
3
3
  import { createConfigurationError, createNetworkError, createServerError, createJSONError, createDataError, createNoResultsMessage } from "./error-handler.js";
4
- export async function performWebSearch(server, query, pageno = 1, time_range, language = "all", safesearch) {
4
+ export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language = "all", safesearch) {
5
5
  const startTime = Date.now();
6
6
  // Build detailed log message with all parameters
7
7
  const searchParams = [
@@ -10,10 +10,10 @@ export async function performWebSearch(server, query, pageno = 1, time_range, la
10
10
  time_range ? `time: ${time_range}` : null,
11
11
  safesearch ? `safesearch: ${safesearch}` : null
12
12
  ].filter(Boolean).join(", ");
13
- logMessage(server, "info", `Starting web search: "${query}" (${searchParams})`);
13
+ logMessage(mcpServer, "info", `Starting web search: "${query}" (${searchParams})`);
14
14
  const searxngUrl = process.env.SEARXNG_URL;
15
15
  if (!searxngUrl) {
16
- logMessage(server, "error", "SEARXNG_URL not configured");
16
+ logMessage(mcpServer, "error", "SEARXNG_URL not configured");
17
17
  throw createConfigurationError("SEARXNG_URL not set. Set it to your SearXNG instance (e.g., http://localhost:8080 or https://search.example.com)");
18
18
  }
19
19
  // Validate that searxngUrl is a valid URL
@@ -44,7 +44,7 @@ export async function performWebSearch(server, query, pageno = 1, time_range, la
44
44
  };
45
45
  // Add proxy dispatcher if proxy is configured
46
46
  // Node.js fetch uses 'dispatcher' option for proxy, not 'agent'
47
- const proxyAgent = createProxyAgent(url.toString());
47
+ const proxyAgent = createProxyAgent(url.toString(), ProxyType.SEARCH);
48
48
  if (proxyAgent) {
49
49
  requestOptions.dispatcher = proxyAgent;
50
50
  }
@@ -69,11 +69,11 @@ export async function performWebSearch(server, query, pageno = 1, time_range, la
69
69
  // Fetch with enhanced error handling
70
70
  let response;
71
71
  try {
72
- logMessage(server, "info", `Making request to: ${url.toString()}`);
72
+ logMessage(mcpServer, "info", `Making request to: ${url.toString()}`);
73
73
  response = await fetch(url.toString(), requestOptions);
74
74
  }
75
75
  catch (error) {
76
- logMessage(server, "error", `Network error during search request: ${error.message}`, { query, url: url.toString() });
76
+ logMessage(mcpServer, "error", `Network error during search request: ${error.message}`, { query, url: url.toString() });
77
77
  const context = {
78
78
  url: url.toString(),
79
79
  searxngUrl,
@@ -123,11 +123,11 @@ export async function performWebSearch(server, query, pageno = 1, time_range, la
123
123
  score: result.score || 0,
124
124
  }));
125
125
  if (results.length === 0) {
126
- logMessage(server, "info", `No results found for query: "${query}"`);
126
+ logMessage(mcpServer, "info", `No results found for query: "${query}"`);
127
127
  return createNoResultsMessage(query);
128
128
  }
129
129
  const duration = Date.now() - startTime;
130
- logMessage(server, "info", `Search completed: "${query}" (${searchParams}) - ${results.length} results in ${duration}ms`);
130
+ logMessage(mcpServer, "info", `Search completed: "${query}" (${searchParams}) - ${results.length} results in ${duration}ms`);
131
131
  return results
132
132
  .map((r) => `Title: ${r.title}\nDescription: ${r.content}\nURL: ${r.url}\nRelevance Score: ${r.score.toFixed(3)}`)
133
133
  .join("\n\n");
@@ -1,4 +1,4 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  interface PaginationOptions {
3
3
  startChar?: number;
4
4
  maxLength?: number;
@@ -6,5 +6,5 @@ interface PaginationOptions {
6
6
  paragraphRange?: string;
7
7
  readHeadings?: boolean;
8
8
  }
9
- export declare function fetchAndConvertToMarkdown(server: Server, url: string, timeoutMs?: number, paginationOptions?: PaginationOptions): Promise<string>;
9
+ export declare function fetchAndConvertToMarkdown(mcpServer: McpServer, url: string, timeoutMs?: number, paginationOptions?: PaginationOptions): Promise<string>;
10
10
  export {};
@@ -1,5 +1,5 @@
1
1
  import { NodeHtmlMarkdown } from "node-html-markdown";
2
- import { createProxyAgent } from "./proxy.js";
2
+ import { createProxyAgent, ProxyType } from "./proxy.js";
3
3
  import { logMessage } from "./logging.js";
4
4
  import { urlCache } from "./cache.js";
5
5
  import { createURLFormatError, createNetworkError, createServerError, createContentError, createConversionError, createTimeoutError, createEmptyContentWarning, createUnexpectedError } from "./error-handler.js";
@@ -100,16 +100,16 @@ function applyPaginationOptions(markdownContent, options) {
100
100
  }
101
101
  return result;
102
102
  }
103
- export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000, paginationOptions = {}) {
103
+ export async function fetchAndConvertToMarkdown(mcpServer, url, timeoutMs = 10000, paginationOptions = {}) {
104
104
  const startTime = Date.now();
105
- logMessage(server, "info", `Fetching URL: ${url}`);
105
+ logMessage(mcpServer, "info", `Fetching URL: ${url}`);
106
106
  // Check cache first
107
107
  const cachedEntry = urlCache.get(url);
108
108
  if (cachedEntry) {
109
- logMessage(server, "info", `Using cached content for URL: ${url}`);
109
+ logMessage(mcpServer, "info", `Using cached content for URL: ${url}`);
110
110
  const result = applyPaginationOptions(cachedEntry.markdownContent, paginationOptions);
111
111
  const duration = Date.now() - startTime;
112
- logMessage(server, "info", `Processed cached URL: ${url} (${result.length} chars in ${duration}ms)`);
112
+ logMessage(mcpServer, "info", `Processed cached URL: ${url} (${result.length} chars in ${duration}ms)`);
113
113
  return result;
114
114
  }
115
115
  // Validate URL format
@@ -118,7 +118,7 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
118
118
  parsedUrl = new URL(url);
119
119
  }
120
120
  catch (error) {
121
- logMessage(server, "error", `Invalid URL format: ${url}`);
121
+ logMessage(mcpServer, "error", `Invalid URL format: ${url}`);
122
122
  throw createURLFormatError(url);
123
123
  }
124
124
  // Create an AbortController instance
@@ -131,10 +131,18 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
131
131
  };
132
132
  // Add proxy dispatcher if proxy is configured
133
133
  // Node.js fetch uses 'dispatcher' option for proxy, not 'agent'
134
- const proxyAgent = createProxyAgent(url);
134
+ const proxyAgent = createProxyAgent(url, ProxyType.URL_READER);
135
135
  if (proxyAgent) {
136
136
  requestOptions.dispatcher = proxyAgent;
137
137
  }
138
+ // Add User-Agent header if configured (URL_READER_USER_AGENT takes priority over USER_AGENT)
139
+ const userAgent = process.env.URL_READER_USER_AGENT || process.env.USER_AGENT;
140
+ if (userAgent) {
141
+ requestOptions.headers = {
142
+ ...requestOptions.headers,
143
+ 'User-Agent': userAgent
144
+ };
145
+ }
138
146
  let response;
139
147
  try {
140
148
  // Fetch the URL with the abort signal
@@ -179,7 +187,7 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
179
187
  throw createConversionError(error, url, htmlContent);
180
188
  }
181
189
  if (!markdownContent || markdownContent.trim().length === 0) {
182
- logMessage(server, "warning", `Empty content after conversion: ${url}`);
190
+ logMessage(mcpServer, "warning", `Empty content after conversion: ${url}`);
183
191
  // DON'T cache empty/failed conversions - return warning directly
184
192
  return createEmptyContentWarning(url, htmlContent.length, htmlContent);
185
193
  }
@@ -188,21 +196,21 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
188
196
  // Apply pagination options
189
197
  const result = applyPaginationOptions(markdownContent, paginationOptions);
190
198
  const duration = Date.now() - startTime;
191
- logMessage(server, "info", `Successfully fetched and converted URL: ${url} (${result.length} chars in ${duration}ms)`);
199
+ logMessage(mcpServer, "info", `Successfully fetched and converted URL: ${url} (${result.length} chars in ${duration}ms)`);
192
200
  return result;
193
201
  }
194
202
  catch (error) {
195
203
  if (error.name === "AbortError") {
196
- logMessage(server, "error", `Timeout fetching URL: ${url} (${timeoutMs}ms)`);
204
+ logMessage(mcpServer, "error", `Timeout fetching URL: ${url} (${timeoutMs}ms)`);
197
205
  throw createTimeoutError(timeoutMs, url);
198
206
  }
199
207
  // Re-throw our enhanced errors
200
208
  if (error.name === 'MCPSearXNGError') {
201
- logMessage(server, "error", `Error fetching URL: ${url} - ${error.message}`);
209
+ logMessage(mcpServer, "error", `Error fetching URL: ${url} - ${error.message}`);
202
210
  throw error;
203
211
  }
204
212
  // Catch any unexpected errors
205
- logMessage(server, "error", `Unexpected error fetching URL: ${url}`, error);
213
+ logMessage(mcpServer, "error", `Unexpected error fetching URL: ${url}`, error);
206
214
  const context = { url };
207
215
  throw createUnexpectedError(error, context);
208
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-searxng",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "MCP server for SearXNG integration",
5
5
  "license": "MIT",
6
6
  "author": "Ihor Sokoliuk (https://github.com/ihor-sokoliuk)",
@@ -36,29 +36,33 @@
36
36
  "scripts": {
37
37
  "build": "tsc && shx chmod +x dist/*.js",
38
38
  "watch": "tsc --watch",
39
- "test": "SEARXNG_URL=https://test-searx.example.com tsx __tests__/run-all.ts",
40
- "test:coverage": "SEARXNG_URL=https://test-searx.example.com c8 --reporter=text tsx __tests__/run-all.ts",
39
+ "test": "cross-env SEARXNG_URL=https://test-searx.example.com tsx __tests__/run-all.ts",
40
+ "test:coverage": "cross-env SEARXNG_URL=https://test-searx.example.com c8 --reporter=text tsx __tests__/run-all.ts",
41
41
  "bootstrap": "npm install && npm run build",
42
42
  "inspector": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/index.js",
43
+ "lint": "eslint src __tests__",
43
44
  "postversion": "TAG=$(node scripts/update-version.js | tail -1) && git add src/index.ts && git commit --amend --no-edit && git tag -f $TAG"
44
45
  },
45
46
  "dependencies": {
46
- "@modelcontextprotocol/sdk": "1.17.4",
47
+ "@modelcontextprotocol/sdk": "1.29.0",
47
48
  "@types/cors": "^2.8.19",
48
- "@types/express": "^5.0.3",
49
- "cors": "^2.8.5",
50
- "express": "^5.1.0",
51
- "node-html-markdown": "^1.3.0",
52
- "undici": "^6.20.1"
49
+ "@types/express": "^5.0.6",
50
+ "cors": "^2.8.6",
51
+ "express": "^5.2.1",
52
+ "node-html-markdown": "^2.0.0",
53
+ "undici": "^7.0.0"
53
54
  },
54
55
  "devDependencies": {
55
- "mcp-evals": "^1.0.18",
56
56
  "@types/node": "^22.17.2",
57
- "@types/supertest": "^6.0.3",
58
- "c8": "^10.1.3",
57
+ "@types/supertest": "^7.2.0",
58
+ "@typescript-eslint/eslint-plugin": "^8.58.0",
59
+ "@typescript-eslint/parser": "^8.58.0",
60
+ "c8": "^11.0.0",
61
+ "cross-env": "^10.1.0",
62
+ "eslint": "^10.1.0",
59
63
  "shx": "^0.4.0",
60
- "supertest": "^7.1.4",
61
- "tsx": "^4.20.5",
64
+ "supertest": "^7.2.2",
65
+ "tsx": "^4.21.0",
62
66
  "typescript": "^5.8.3"
63
67
  }
64
68
  }