mcp-searxng 1.2.1 → 1.3.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 +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +14 -0
- package/dist/http-server.d.ts +2 -1
- package/dist/http-server.js +41 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -30
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -46,6 +46,17 @@ Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g.
|
|
|
46
46
|
- **Safe Search**: Control content filtering level for search results.
|
|
47
47
|
- **Relevance Filtering**: Filter out low-scoring search results with `min_score`.
|
|
48
48
|
|
|
49
|
+
## Why mcp-searxng?
|
|
50
|
+
|
|
51
|
+
| | Brave MCP | Exa MCP | Firecrawl MCP | **mcp-searxng** |
|
|
52
|
+
|--|:---------:|:-------:|:-------------:|:---------------:|
|
|
53
|
+
| Web Search | ✓ | ✓ | ✓ | ✓ |
|
|
54
|
+
| Read URL | ✗ | ✓ | ✓ | ✓ |
|
|
55
|
+
| Pagination | ✗ | ✗ | ✓ | ✓ |
|
|
56
|
+
| Self-hosted | ✗ | ✗ | Partial | ✓ |
|
|
57
|
+
| Privacy | ✗ | ✗ | ✗ | ✓ |
|
|
58
|
+
| Free / No API key | ✗ | ✗ | ✗ | ✓ |
|
|
59
|
+
|
|
49
60
|
## How It Works
|
|
50
61
|
|
|
51
62
|
`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 +251,16 @@ See also: [SearXNG settings docs](https://docs.searxng.org/admin/settings/settin
|
|
|
240
251
|
|
|
241
252
|
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
242
253
|
|
|
254
|
+
## Star History
|
|
255
|
+
|
|
256
|
+
<a href="https://www.star-history.com/?repos=ihor-sokoliuk%2Fmcp-searxng&type=date&legend=top-left">
|
|
257
|
+
<picture>
|
|
258
|
+
<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" />
|
|
259
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ihor-sokoliuk/mcp-searxng&type=date&legend=top-left" />
|
|
260
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ihor-sokoliuk/mcp-searxng&type=date&legend=top-left" />
|
|
261
|
+
</picture>
|
|
262
|
+
</a>
|
|
263
|
+
|
|
243
264
|
## License
|
|
244
265
|
|
|
245
266
|
MIT — see [LICENSE](LICENSE) for details.
|
package/dist/cli.d.ts
ADDED
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
|
+
});
|
package/dist/http-server.d.ts
CHANGED
|
@@ -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 "
|
|
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>;
|
package/dist/http-server.js
CHANGED
|
@@ -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 "
|
|
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 "
|
|
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
|
+
declare const packageVersion = "1.3.0";
|
|
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.
|
|
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.0";
|
|
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.
|
|
3
|
+
"version": "1.3.0",
|
|
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/
|
|
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/
|
|
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
|
},
|