mcp-searxng 1.2.1 → 1.3.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
@@ -10,6 +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
  [![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)
13
+ [![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
14
 
14
15
  An [MCP server](https://modelcontextprotocol.io/introduction) that integrates the [SearXNG](https://docs.searxng.org) API, giving AI assistants web search capabilities.
15
16
 
@@ -46,6 +47,17 @@ Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g.
46
47
  - **Safe Search**: Control content filtering level for search results.
47
48
  - **Relevance Filtering**: Filter out low-scoring search results with `min_score`.
48
49
 
50
+ ## Why mcp-searxng?
51
+
52
+ | | Brave MCP | Exa MCP | Firecrawl MCP | **mcp-searxng** |
53
+ |--|:---------:|:-------:|:-------------:|:---------------:|
54
+ | Web Search | ✓ | ✓ | ✓ | ✓ |
55
+ | Read URL | ✗ | ✓ | ✓ | ✓ |
56
+ | Pagination | ✗ | ✗ | ✓ | ✓ |
57
+ | Self-hosted | ✗ | ✗ | Partial | ✓ |
58
+ | Privacy | ✗ | ✗ | ✗ | ✓ |
59
+ | Free / No API key | ✗ | ✗ | ✗ | ✓ |
60
+
49
61
  ## How It Works
50
62
 
51
63
  `mcp-searxng` is a standalone MCP server — a separate Node.js process that your AI assistant connects to for web search. It queries any SearXNG instance via its HTTP JSON API.
@@ -240,6 +252,16 @@ See also: [SearXNG settings docs](https://docs.searxng.org/admin/settings/settin
240
252
 
241
253
  See [CONTRIBUTING.md](CONTRIBUTING.md)
242
254
 
255
+ ## Star History
256
+
257
+ <a href="https://www.star-history.com/?repos=ihor-sokoliuk%2Fmcp-searxng&type=date&legend=top-left">
258
+ <picture>
259
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ihor-sokoliuk/mcp-searxng&type=date&theme=dark&legend=top-left" />
260
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ihor-sokoliuk/mcp-searxng&type=date&legend=top-left" />
261
+ <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ihor-sokoliuk/mcp-searxng&type=date&legend=top-left" />
262
+ </picture>
263
+ </a>
264
+
243
265
  ## License
244
266
 
245
267
  MIT — see [LICENSE](LICENSE) for details.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./index.js";
3
+ process.on('uncaughtException', (error) => {
4
+ console.error('Uncaught Exception:', error);
5
+ process.exit(1);
6
+ });
7
+ process.on('unhandledRejection', (reason, promise) => {
8
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
9
+ process.exit(1);
10
+ });
11
+ main().catch((error) => {
12
+ console.error("Failed to start server:", error);
13
+ process.exit(1);
14
+ });
@@ -2,7 +2,8 @@ import express from "express";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  /**
4
4
  * Resolves the bind host from the MCP_HTTP_HOST environment variable.
5
- * Falls back to "0.0.0.0" (all interfaces) when the variable is absent or whitespace-only.
5
+ * Falls back to "127.0.0.1" (localhost only) when the variable is absent or whitespace-only.
6
+ * Set MCP_HTTP_HOST=0.0.0.0 to expose on all interfaces (e.g. Docker, remote access).
6
7
  */
7
8
  export declare function resolveBindHost(envValue: string | undefined): string;
8
9
  export declare function createHttpServer(createMcpServer: () => McpServer): Promise<express.Application>;
@@ -1,5 +1,6 @@
1
1
  import express from "express";
2
2
  import cors from "cors";
3
+ import rateLimit from "express-rate-limit";
3
4
  import { randomUUID } from "crypto";
4
5
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
6
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
@@ -8,15 +9,48 @@ import { packageVersion } from "./index.js";
8
9
  import { getHttpSecurityConfig, isOriginAllowed, isRequestAuthorized, validateHttpSecurityConfig, } from "./http-security.js";
9
10
  /**
10
11
  * Resolves the bind host from the MCP_HTTP_HOST environment variable.
11
- * Falls back to "0.0.0.0" (all interfaces) when the variable is absent or whitespace-only.
12
+ * Falls back to "127.0.0.1" (localhost only) when the variable is absent or whitespace-only.
13
+ * Set MCP_HTTP_HOST=0.0.0.0 to expose on all interfaces (e.g. Docker, remote access).
12
14
  */
13
15
  export function resolveBindHost(envValue) {
14
16
  const trimmed = envValue?.trim();
15
17
  if (!trimmed) {
16
- return "0.0.0.0";
18
+ return "127.0.0.1";
17
19
  }
18
20
  return trimmed;
19
21
  }
22
+ function makeRateLimiters() {
23
+ const windowMs = parseInt(process.env.MCP_RATE_WINDOW_MS ?? "60000", 10);
24
+ const initLimiter = rateLimit({
25
+ windowMs,
26
+ max: parseInt(process.env.MCP_RATE_INIT_MAX ?? "20", 10),
27
+ standardHeaders: true,
28
+ legacyHeaders: false,
29
+ message: {
30
+ jsonrpc: "2.0",
31
+ error: { code: -32029, message: "Too many requests" },
32
+ id: null,
33
+ },
34
+ });
35
+ const sessionLimiter = rateLimit({
36
+ windowMs,
37
+ max: parseInt(process.env.MCP_RATE_SESSION_MAX ?? "300", 10),
38
+ standardHeaders: true,
39
+ legacyHeaders: false,
40
+ message: {
41
+ jsonrpc: "2.0",
42
+ error: { code: -32029, message: "Too many requests" },
43
+ id: null,
44
+ },
45
+ });
46
+ const healthLimiter = rateLimit({
47
+ windowMs: 60000,
48
+ max: 60,
49
+ standardHeaders: true,
50
+ legacyHeaders: false,
51
+ });
52
+ return { initLimiter, sessionLimiter, healthLimiter };
53
+ }
20
54
  export async function createHttpServer(createMcpServer) {
21
55
  const app = express();
22
56
  const security = getHttpSecurityConfig();
@@ -44,10 +78,11 @@ export async function createHttpServer(createMcpServer) {
44
78
  id: null,
45
79
  });
46
80
  }
81
+ const { initLimiter, sessionLimiter, healthLimiter } = makeRateLimiters();
47
82
  // Map to store sessions by session ID
48
83
  const sessions = new Map();
49
84
  // Handle POST requests for client-to-server communication
50
- app.post('/mcp', async (req, res) => {
85
+ app.post('/mcp', initLimiter, sessionLimiter, async (req, res) => {
51
86
  if (!isRequestAuthorized(req.headers.authorization, security)) {
52
87
  rejectUnauthorized(res);
53
88
  return;
@@ -123,7 +158,7 @@ export async function createHttpServer(createMcpServer) {
123
158
  }
124
159
  });
125
160
  // Handle GET requests for server-to-client notifications via SSE
126
- app.get('/mcp', async (req, res) => {
161
+ app.get('/mcp', sessionLimiter, async (req, res) => {
127
162
  if (!isRequestAuthorized(req.headers.authorization, security)) {
128
163
  rejectUnauthorized(res);
129
164
  return;
@@ -152,7 +187,7 @@ export async function createHttpServer(createMcpServer) {
152
187
  }
153
188
  });
154
189
  // Handle DELETE requests for session termination
155
- app.delete('/mcp', async (req, res) => {
190
+ app.delete('/mcp', sessionLimiter, async (req, res) => {
156
191
  if (!isRequestAuthorized(req.headers.authorization, security)) {
157
192
  rejectUnauthorized(res);
158
193
  return;
@@ -184,7 +219,7 @@ export async function createHttpServer(createMcpServer) {
184
219
  }
185
220
  });
186
221
  // Health check endpoint
187
- app.get('/health', (_req, res) => {
222
+ app.get('/health', healthLimiter, (_req, res) => {
188
223
  res.json({
189
224
  status: 'healthy',
190
225
  server: 'ihor-sokoliuk/mcp-searxng',
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- #!/usr/bin/env node
2
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- declare const packageVersion = "1.2.1";
2
+ declare const packageVersion = "1.3.1";
4
3
  export { packageVersion };
5
4
  export declare function isWebUrlReadArgs(args: unknown): args is {
6
5
  url: string;
@@ -15,3 +14,4 @@ export declare function isWebUrlReadArgs(args: unknown): args is {
15
14
  * Called once per HTTP session, or once for STDIO mode.
16
15
  */
17
16
  export declare function createMcpServer(): McpServer;
17
+ export declare function main(): Promise<void>;
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- #!/usr/bin/env node
2
- import { fileURLToPath } from "node:url";
3
- import { realpathSync } from "node:fs";
4
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
3
  import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -12,17 +9,7 @@ import { fetchAndConvertToMarkdown } from "./url-reader.js";
12
9
  import { createConfigResource, createHelpResource } from "./resources.js";
13
10
  import { createHttpServer, resolveBindHost } from "./http-server.js";
14
11
  // Use a static version string that will be updated by the version script
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
- })();
12
+ const packageVersion = "1.3.1";
26
13
  // Export the version for use in other modules
27
14
  export { packageVersion };
28
15
  // Type guard for URL reading args
@@ -198,7 +185,7 @@ export function createMcpServer() {
198
185
  return mcpServer;
199
186
  }
200
187
  // Main function
201
- async function main() {
188
+ export async function main() {
202
189
  // Check for HTTP transport mode
203
190
  const httpPort = process.env.MCP_HTTP_PORT;
204
191
  if (httpPort) {
@@ -250,18 +237,3 @@ async function main() {
250
237
  logMessage(mcpServer, "info", `SearXNG URL: ${process.env.SEARXNG_URL || 'not configured'}`);
251
238
  }
252
239
  }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-searxng",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
5
5
  "description": "MCP server for SearXNG integration",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "type": "module",
27
27
  "bin": {
28
- "mcp-searxng": "dist/index.js"
28
+ "mcp-searxng": "dist/cli.js"
29
29
  },
30
30
  "main": "dist/index.js",
31
31
  "files": [
@@ -38,9 +38,9 @@
38
38
  "build": "tsc && shx chmod +x dist/*.js",
39
39
  "watch": "tsc --watch",
40
40
  "test": "cross-env SEARXNG_URL=https://test-searx.example.com tsx __tests__/run-all.ts",
41
- "test:coverage": "cross-env SEARXNG_URL=https://test-searx.example.com c8 --reporter=text tsx __tests__/run-all.ts",
41
+ "test:coverage": "cross-env SEARXNG_URL=https://test-searx.example.com c8 --reporter=text --exclude 'dist/**' tsx __tests__/run-all.ts",
42
42
  "bootstrap": "npm install && npm run build",
43
- "inspector": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/index.js",
43
+ "inspector": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/cli.js",
44
44
  "lint": "eslint src __tests__",
45
45
  "audit:deps": "npm audit --audit-level=moderate",
46
46
  "security": "npm run lint && npm run audit:deps",
@@ -52,6 +52,7 @@
52
52
  "@types/express": "^5.0.6",
53
53
  "cors": "^2.8.6",
54
54
  "express": "^5.2.1",
55
+ "express-rate-limit": "^8.5.2",
55
56
  "node-html-markdown": "^2.0.0",
56
57
  "undici": "^7.24.0"
57
58
  },