mcp-searxng 1.4.0 → 1.6.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 -1
- package/dist/cache.d.ts +5 -1
- package/dist/cache.js +44 -6
- package/dist/index.js +35 -3
- package/dist/instance-info.d.ts +5 -0
- package/dist/instance-info.js +183 -0
- package/dist/resources.js +47 -3
- package/dist/search.d.ts +1 -1
- package/dist/search.js +188 -19
- package/dist/suggestions.d.ts +2 -0
- package/dist/suggestions.js +34 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +126 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -40,6 +40,10 @@ Replace `YOUR_SEARXNG_INSTANCE_URL` with the URL of your SearXNG instance (e.g.
|
|
|
40
40
|
## Features
|
|
41
41
|
|
|
42
42
|
- **Web Search**: General queries, news, articles, with pagination.
|
|
43
|
+
- **Structured Search Output**: Choose formatted text or raw SearXNG-shaped JSON with `response_format`.
|
|
44
|
+
- **Direct Answers & Metadata**: Text results surface SearXNG answers, corrections, suggestions, and infoboxes before result lists.
|
|
45
|
+
- **Search Suggestions**: Query autocomplete via SearXNG's `/autocompleter` endpoint.
|
|
46
|
+
- **Instance Capability Discovery**: Inspect configured categories, engines, defaults, locales, and plugins from `/config`.
|
|
43
47
|
- **URL Content Reading**: Advanced content extraction with pagination, section filtering, and heading extraction.
|
|
44
48
|
- **Intelligent Caching**: URL content is cached with TTL (Time-To-Live) to improve performance and reduce redundant requests.
|
|
45
49
|
- **Pagination**: Control which page of results to retrieve.
|
|
@@ -87,7 +91,23 @@ AI Assistant (e.g. Claude)
|
|
|
87
91
|
- `safesearch` (number, optional): Safe search filter level (0: None, 1: Moderate, 2: Strict) (default: instance setting)
|
|
88
92
|
- `min_score` (number, optional): Minimum relevance score from 0.0 to 1.0. Results below this score are filtered out.
|
|
89
93
|
- `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"`).
|
|
94
|
+
- `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`). When live `/config` is available, values are trimmed and normalized case-insensitively to the instance's canonical category names; unknown values are rejected with available categories listed. If `/config` is unavailable, values are forwarded as-is with a warning. Default: SearXNG instance default.
|
|
95
|
+
- `engines` (string, optional): Comma-separated SearXNG engine names (e.g. `"google,bing,ddg"`, `"semantic scholar"`). When live `/config` is available, values are trimmed and normalized case-insensitively to canonical engine names, including engines disabled by default; unknown values are rejected with available engines listed. If `/config` is unavailable, values are forwarded as-is with a warning.
|
|
96
|
+
- `response_format` (string, optional): Response format, either `"text"` for formatted agent-readable output or `"json"` for raw SearXNG JSON with filtered/sliced `results`. (default: `"text"`)
|
|
97
|
+
|
|
98
|
+
- **searxng_search_suggestions**
|
|
99
|
+
- Get autocomplete suggestions for refining search queries
|
|
100
|
+
- Inputs:
|
|
101
|
+
- `query` (string): Partial or complete query to autocomplete.
|
|
102
|
+
- `language` (string, optional): Language code for suggestions (e.g., "en", "fr", "de") or "all" (default: "all")
|
|
103
|
+
|
|
104
|
+
- **searxng_instance_info**
|
|
105
|
+
- Discover categories, engines, defaults, locales, and plugins exposed by the configured SearXNG instance
|
|
106
|
+
- Inputs:
|
|
107
|
+
- `includeEngines` (boolean, optional): Include enabled engine names in the response. (default: false)
|
|
108
|
+
- `includeDisabled` (boolean, optional): Include disabled engine names when `includeEngines` is true. (default: false)
|
|
109
|
+
- `category` (string, optional): Filter categories and engines to a single category name.
|
|
110
|
+
- `refresh` (boolean, optional): Bypass the process cache and fetch fresh `/config` data. (default: false)
|
|
91
111
|
|
|
92
112
|
- **web_url_read**
|
|
93
113
|
- Read and convert the content from a URL to markdown with advanced content extraction options
|
package/dist/cache.d.ts
CHANGED
|
@@ -2,14 +2,17 @@ interface CacheEntry {
|
|
|
2
2
|
htmlContent: string;
|
|
3
3
|
markdownContent: string;
|
|
4
4
|
timestamp: number;
|
|
5
|
+
hitCount: number;
|
|
5
6
|
}
|
|
6
7
|
declare class SimpleCache {
|
|
7
8
|
private cache;
|
|
8
9
|
private readonly ttlMs;
|
|
10
|
+
private readonly maxEntries;
|
|
9
11
|
private cleanupInterval;
|
|
10
|
-
constructor(ttlMs?: number, cleanupIntervalMs?: number);
|
|
12
|
+
constructor(ttlMs?: number, maxEntries?: number, cleanupIntervalMs?: number);
|
|
11
13
|
private startCleanup;
|
|
12
14
|
private cleanupExpired;
|
|
15
|
+
private evictIfNeeded;
|
|
13
16
|
get(url: string): CacheEntry | null;
|
|
14
17
|
set(url: string, htmlContent: string, markdownContent: string): void;
|
|
15
18
|
clear(): void;
|
|
@@ -19,6 +22,7 @@ declare class SimpleCache {
|
|
|
19
22
|
entries: Array<{
|
|
20
23
|
url: string;
|
|
21
24
|
age: number;
|
|
25
|
+
hitCount: number;
|
|
22
26
|
}>;
|
|
23
27
|
};
|
|
24
28
|
}
|
package/dist/cache.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
const DEFAULT_CACHE_TTL_MS = 86400000;
|
|
2
|
+
const DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
3
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 60000;
|
|
4
|
+
function parsePositiveInteger(value, fallback) {
|
|
5
|
+
if (value === undefined || value.trim() === "") {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
const parsed = parseInt(value, 10);
|
|
9
|
+
return Number.isNaN(parsed) || parsed <= 0 ? fallback : parsed;
|
|
10
|
+
}
|
|
11
|
+
function normalizePositiveInteger(value, fallback) {
|
|
12
|
+
return !Number.isFinite(value) || !Number.isInteger(value) || value <= 0 ? fallback : value;
|
|
13
|
+
}
|
|
1
14
|
class SimpleCache {
|
|
2
15
|
cache = new Map();
|
|
3
16
|
ttlMs;
|
|
17
|
+
maxEntries;
|
|
4
18
|
cleanupInterval = null;
|
|
5
|
-
constructor(ttlMs =
|
|
6
|
-
this.ttlMs = ttlMs;
|
|
7
|
-
this.
|
|
19
|
+
constructor(ttlMs = parsePositiveInteger(process.env.CACHE_TTL_MS, DEFAULT_CACHE_TTL_MS), maxEntries = parsePositiveInteger(process.env.CACHE_MAX_ENTRIES, DEFAULT_CACHE_MAX_ENTRIES), cleanupIntervalMs = DEFAULT_CLEANUP_INTERVAL_MS) {
|
|
20
|
+
this.ttlMs = normalizePositiveInteger(ttlMs, DEFAULT_CACHE_TTL_MS);
|
|
21
|
+
this.maxEntries = normalizePositiveInteger(maxEntries, DEFAULT_CACHE_MAX_ENTRIES);
|
|
22
|
+
this.startCleanup(normalizePositiveInteger(cleanupIntervalMs, DEFAULT_CLEANUP_INTERVAL_MS));
|
|
8
23
|
}
|
|
9
24
|
startCleanup(cleanupIntervalMs) {
|
|
10
|
-
// Clean up expired entries every cleanupIntervalMs milliseconds (default
|
|
25
|
+
// Clean up expired entries every cleanupIntervalMs milliseconds (default 60s)
|
|
11
26
|
this.cleanupInterval = setInterval(() => {
|
|
12
27
|
this.cleanupExpired();
|
|
13
28
|
}, cleanupIntervalMs);
|
|
@@ -21,6 +36,25 @@ class SimpleCache {
|
|
|
21
36
|
}
|
|
22
37
|
}
|
|
23
38
|
}
|
|
39
|
+
evictIfNeeded() {
|
|
40
|
+
this.cleanupExpired();
|
|
41
|
+
while (this.cache.size > this.maxEntries) {
|
|
42
|
+
let evictionKey = null;
|
|
43
|
+
let evictionEntry = null;
|
|
44
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
45
|
+
if (evictionEntry === null ||
|
|
46
|
+
entry.hitCount < evictionEntry.hitCount ||
|
|
47
|
+
(entry.hitCount === evictionEntry.hitCount && entry.timestamp < evictionEntry.timestamp)) {
|
|
48
|
+
evictionKey = key;
|
|
49
|
+
evictionEntry = entry;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (evictionKey === null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.cache.delete(evictionKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
24
58
|
get(url) {
|
|
25
59
|
const entry = this.cache.get(url);
|
|
26
60
|
if (!entry) {
|
|
@@ -31,14 +65,17 @@ class SimpleCache {
|
|
|
31
65
|
this.cache.delete(url);
|
|
32
66
|
return null;
|
|
33
67
|
}
|
|
68
|
+
entry.hitCount++;
|
|
34
69
|
return entry;
|
|
35
70
|
}
|
|
36
71
|
set(url, htmlContent, markdownContent) {
|
|
37
72
|
this.cache.set(url, {
|
|
38
73
|
htmlContent,
|
|
39
74
|
markdownContent,
|
|
40
|
-
timestamp: Date.now()
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
hitCount: 0
|
|
41
77
|
});
|
|
78
|
+
this.evictIfNeeded();
|
|
42
79
|
}
|
|
43
80
|
clear() {
|
|
44
81
|
this.cache.clear();
|
|
@@ -55,7 +92,8 @@ class SimpleCache {
|
|
|
55
92
|
const now = Date.now();
|
|
56
93
|
const entries = Array.from(this.cache.entries()).map(([url, entry]) => ({
|
|
57
94
|
url,
|
|
58
|
-
age: now - entry.timestamp
|
|
95
|
+
age: now - entry.timestamp,
|
|
96
|
+
hitCount: entry.hitCount
|
|
59
97
|
}));
|
|
60
98
|
return {
|
|
61
99
|
size: this.cache.size,
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,11 @@ 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, LITE_WEB_SEARCH_TOOL, LITE_READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
|
|
5
|
+
import { WEB_SEARCH_TOOL, SUGGESTIONS_TOOL, INSTANCE_INFO_TOOL, READ_URL_TOOL, LITE_WEB_SEARCH_TOOL, LITE_SUGGESTIONS_TOOL, LITE_INSTANCE_INFO_TOOL, LITE_READ_URL_TOOL, isSearXNGWebSearchArgs, isSearXNGSearchSuggestionsArgs, isSearXNGInstanceInfoArgs, } from "./types.js";
|
|
6
6
|
import { logMessage, setLogLevel, getCurrentLogLevel } from "./logging.js";
|
|
7
7
|
import { performWebSearch } from "./search.js";
|
|
8
|
+
import { performSearchSuggestions } from "./suggestions.js";
|
|
9
|
+
import { fetchInstanceInfo } from "./instance-info.js";
|
|
8
10
|
import { fetchAndConvertToMarkdown } from "./url-reader.js";
|
|
9
11
|
import { createConfigResource, createHelpResource } from "./resources.js";
|
|
10
12
|
import { createHttpServer, resolveBindHost } from "./http-server.js";
|
|
@@ -83,12 +85,14 @@ export function createMcpServer() {
|
|
|
83
85
|
const server = mcpServer.server;
|
|
84
86
|
const useLiteTools = process.env.SEARXNG_LITE_TOOLS === "true";
|
|
85
87
|
const searchTool = useLiteTools ? LITE_WEB_SEARCH_TOOL : WEB_SEARCH_TOOL;
|
|
88
|
+
const suggestionsTool = useLiteTools ? LITE_SUGGESTIONS_TOOL : SUGGESTIONS_TOOL;
|
|
89
|
+
const instanceInfoTool = useLiteTools ? LITE_INSTANCE_INFO_TOOL : INSTANCE_INFO_TOOL;
|
|
86
90
|
const readUrlTool = useLiteTools ? LITE_READ_URL_TOOL : READ_URL_TOOL;
|
|
87
91
|
// List tools handler
|
|
88
92
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
89
93
|
logMessage(mcpServer, "debug", "Handling list_tools request");
|
|
90
94
|
return {
|
|
91
|
-
tools: [searchTool, readUrlTool],
|
|
95
|
+
tools: [searchTool, suggestionsTool, instanceInfoTool, readUrlTool],
|
|
92
96
|
};
|
|
93
97
|
});
|
|
94
98
|
// Call tool handler
|
|
@@ -100,7 +104,35 @@ export function createMcpServer() {
|
|
|
100
104
|
if (!isSearXNGWebSearchArgs(args)) {
|
|
101
105
|
throw new Error("Invalid arguments for web search");
|
|
102
106
|
}
|
|
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);
|
|
107
|
+
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch, args.min_score, args.num_results, args.categories, args.engines, args.response_format);
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: result,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
else if (name === "searxng_search_suggestions") {
|
|
118
|
+
if (!isSearXNGSearchSuggestionsArgs(args)) {
|
|
119
|
+
throw new Error("Invalid arguments for search suggestions");
|
|
120
|
+
}
|
|
121
|
+
const suggestions = await performSearchSuggestions(mcpServer, args.query, args.language);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: JSON.stringify({ query: args.query, suggestions }, null, 2),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
else if (name === "searxng_instance_info") {
|
|
132
|
+
if (!isSearXNGInstanceInfoArgs(args)) {
|
|
133
|
+
throw new Error("Invalid arguments for instance info");
|
|
134
|
+
}
|
|
135
|
+
const result = await fetchInstanceInfo(mcpServer, args.includeEngines, args.includeDisabled, args.category, args.refresh);
|
|
104
136
|
return {
|
|
105
137
|
content: [
|
|
106
138
|
{
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare function clearInstanceInfoCacheForTests(): void;
|
|
3
|
+
export declare function getKnownEngines(mcpServer: McpServer, refresh?: boolean): Promise<Set<string> | null>;
|
|
4
|
+
export declare function getKnownCategories(mcpServer: McpServer, refresh?: boolean): Promise<Set<string> | null>;
|
|
5
|
+
export declare function fetchInstanceInfo(mcpServer: McpServer, includeEngines?: boolean, includeDisabled?: boolean, category?: string, refresh?: boolean): Promise<string>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { logMessage } from "./logging.js";
|
|
2
|
+
import { createDefaultAgent, createProxyAgent, ProxyType } from "./proxy.js";
|
|
3
|
+
let cachedConfig = null;
|
|
4
|
+
let cachedBaseUrl = null;
|
|
5
|
+
function unavailable(message, status) {
|
|
6
|
+
return JSON.stringify({
|
|
7
|
+
available: false,
|
|
8
|
+
message,
|
|
9
|
+
...(status !== undefined ? { status } : {}),
|
|
10
|
+
}, null, 2);
|
|
11
|
+
}
|
|
12
|
+
function categoryNamesFromEngines(config) {
|
|
13
|
+
const names = new Set();
|
|
14
|
+
if (Array.isArray(config.engines)) {
|
|
15
|
+
for (const engine of config.engines) {
|
|
16
|
+
for (const category of engineCategories(engine)) {
|
|
17
|
+
if (typeof category === "string" && category.trim() !== "") {
|
|
18
|
+
names.add(category);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...names];
|
|
24
|
+
}
|
|
25
|
+
function namesFromCategories(config) {
|
|
26
|
+
const names = new Set();
|
|
27
|
+
if (Array.isArray(config.categories)) {
|
|
28
|
+
for (const category of config.categories) {
|
|
29
|
+
if (typeof category === "string" && category.trim() !== "") {
|
|
30
|
+
names.add(category);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (config.categories && typeof config.categories === "object") {
|
|
35
|
+
for (const category of Object.keys(config.categories)) {
|
|
36
|
+
if (category.trim() !== "") {
|
|
37
|
+
names.add(category);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const category of categoryNamesFromEngines(config)) {
|
|
42
|
+
names.add(category);
|
|
43
|
+
}
|
|
44
|
+
return [...names].sort();
|
|
45
|
+
}
|
|
46
|
+
function engineCategories(engine) {
|
|
47
|
+
if (Array.isArray(engine.categories)) {
|
|
48
|
+
return engine.categories;
|
|
49
|
+
}
|
|
50
|
+
if (typeof engine.category === "string") {
|
|
51
|
+
return [engine.category];
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
function collectEngines(config, includeDisabled, category) {
|
|
56
|
+
const enabled = new Set();
|
|
57
|
+
const disabled = new Set();
|
|
58
|
+
if (Array.isArray(config.engines)) {
|
|
59
|
+
for (const engine of config.engines) {
|
|
60
|
+
if (!engine || typeof engine.name !== "string") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const categories = engineCategories(engine);
|
|
64
|
+
if (category && !categories.includes(category)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (engine.disabled) {
|
|
68
|
+
if (includeDisabled) {
|
|
69
|
+
disabled.add(engine.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
enabled.add(engine.name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
enabled: [...enabled].sort(),
|
|
79
|
+
...(includeDisabled ? { disabled: [...disabled].sort() } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function allEngineNames(config) {
|
|
83
|
+
const names = new Set();
|
|
84
|
+
if (Array.isArray(config.engines)) {
|
|
85
|
+
for (const engine of config.engines) {
|
|
86
|
+
if (engine && typeof engine.name === "string") {
|
|
87
|
+
names.add(engine.name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return names;
|
|
92
|
+
}
|
|
93
|
+
function formatInstanceInfo(config, includeEngines, includeDisabled, category) {
|
|
94
|
+
const categories = category
|
|
95
|
+
? namesFromCategories(config).filter((name) => name === category)
|
|
96
|
+
: namesFromCategories(config);
|
|
97
|
+
const payload = {
|
|
98
|
+
available: true,
|
|
99
|
+
categories,
|
|
100
|
+
defaults: {
|
|
101
|
+
safesearch: config.search?.safe_search ?? config.default_safe_search,
|
|
102
|
+
locale: config.default_locale,
|
|
103
|
+
language: config.default_language,
|
|
104
|
+
theme: config.default_theme,
|
|
105
|
+
},
|
|
106
|
+
locales: config.locales,
|
|
107
|
+
plugins: config.plugins ?? [],
|
|
108
|
+
};
|
|
109
|
+
if (includeEngines) {
|
|
110
|
+
payload.engines = collectEngines(config, includeDisabled, category);
|
|
111
|
+
}
|
|
112
|
+
return JSON.stringify(payload, null, 2);
|
|
113
|
+
}
|
|
114
|
+
export function clearInstanceInfoCacheForTests() {
|
|
115
|
+
cachedConfig = null;
|
|
116
|
+
cachedBaseUrl = null;
|
|
117
|
+
}
|
|
118
|
+
async function fetchConfig(mcpServer, refresh = false) {
|
|
119
|
+
const base = process.env.SEARXNG_URL;
|
|
120
|
+
if (!base) {
|
|
121
|
+
return {
|
|
122
|
+
available: false,
|
|
123
|
+
message: "SEARXNG_URL is not configured; cannot fetch SearXNG /config.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (refresh) {
|
|
127
|
+
cachedConfig = null;
|
|
128
|
+
}
|
|
129
|
+
if (cachedConfig && cachedBaseUrl === base) {
|
|
130
|
+
return { available: true, config: cachedConfig };
|
|
131
|
+
}
|
|
132
|
+
const parsedBase = new URL(base.endsWith("/") ? base : `${base}/`);
|
|
133
|
+
const url = new URL("config", parsedBase);
|
|
134
|
+
try {
|
|
135
|
+
const requestOptions = {
|
|
136
|
+
signal: AbortSignal.timeout(5000),
|
|
137
|
+
};
|
|
138
|
+
const proxyAgent = createProxyAgent(url.toString(), ProxyType.SEARCH);
|
|
139
|
+
const dispatcher = proxyAgent ?? createDefaultAgent();
|
|
140
|
+
if (dispatcher) {
|
|
141
|
+
requestOptions.dispatcher = dispatcher;
|
|
142
|
+
}
|
|
143
|
+
const response = await fetch(url.toString(), requestOptions);
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
return {
|
|
146
|
+
available: false,
|
|
147
|
+
message: `SearXNG /config is unavailable: HTTP ${response.status} ${response.statusText}`,
|
|
148
|
+
status: response.status,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
cachedConfig = await response.json();
|
|
152
|
+
cachedBaseUrl = base;
|
|
153
|
+
return { available: true, config: cachedConfig };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logMessage(mcpServer, "warning", `SearXNG /config fetch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
157
|
+
return {
|
|
158
|
+
available: false,
|
|
159
|
+
message: "SearXNG /config is unavailable; instance capability discovery could not complete.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function getKnownEngines(mcpServer, refresh = false) {
|
|
164
|
+
const result = await fetchConfig(mcpServer, refresh);
|
|
165
|
+
if (!result.available) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return allEngineNames(result.config);
|
|
169
|
+
}
|
|
170
|
+
export async function getKnownCategories(mcpServer, refresh = false) {
|
|
171
|
+
const result = await fetchConfig(mcpServer, refresh);
|
|
172
|
+
if (!result.available) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return new Set(namesFromCategories(result.config));
|
|
176
|
+
}
|
|
177
|
+
export async function fetchInstanceInfo(mcpServer, includeEngines = false, includeDisabled = false, category, refresh = false) {
|
|
178
|
+
const result = await fetchConfig(mcpServer, refresh);
|
|
179
|
+
if (!result.available) {
|
|
180
|
+
return unavailable(result.message, result.status);
|
|
181
|
+
}
|
|
182
|
+
return formatInstanceInfo(result.config, includeEngines, includeDisabled, category);
|
|
183
|
+
}
|
package/dist/resources.js
CHANGED
|
@@ -21,7 +21,12 @@ export function createConfigResource() {
|
|
|
21
21
|
currentLogLevel: getCurrentLogLevel()
|
|
22
22
|
},
|
|
23
23
|
capabilities: {
|
|
24
|
-
tools: [
|
|
24
|
+
tools: [
|
|
25
|
+
"searxng_web_search",
|
|
26
|
+
"searxng_search_suggestions",
|
|
27
|
+
"searxng_instance_info",
|
|
28
|
+
"web_url_read"
|
|
29
|
+
],
|
|
25
30
|
logging: true,
|
|
26
31
|
resources: true,
|
|
27
32
|
transports: process.env.MCP_HTTP_PORT ? ["stdio", "http"] : ["stdio"]
|
|
@@ -33,7 +38,7 @@ export function createHelpResource() {
|
|
|
33
38
|
return `# SearXNG MCP Server Help
|
|
34
39
|
|
|
35
40
|
## Overview
|
|
36
|
-
This is a Model Context Protocol (MCP) server that provides web search
|
|
41
|
+
This is a Model Context Protocol (MCP) server that provides web search, autocomplete suggestions, instance capability discovery, and URL content reading through SearXNG.
|
|
37
42
|
|
|
38
43
|
## Available Tools
|
|
39
44
|
|
|
@@ -47,12 +52,39 @@ Performs web searches using the configured SearXNG instance.
|
|
|
47
52
|
- \`language\` (optional): Language code like "en", "fr", "de" (default: "all")
|
|
48
53
|
- \`safesearch\` (optional): Safe search level - 0 (none), 1 (moderate), 2 (strict)
|
|
49
54
|
- \`min_score\` (optional): Minimum relevance score from 0.0 to 1.0
|
|
55
|
+
- \`num_results\` (optional): Maximum result count from 1 to 20
|
|
56
|
+
- \`categories\` (optional): Comma-separated SearXNG categories such as "news" or "it,science"; live \`/config\` values are normalized case-insensitively when available
|
|
57
|
+
- \`engines\` (optional): Comma-separated SearXNG engine names such as "google,bing,ddg" or "semantic scholar"; live \`/config\` values are normalized case-insensitively when available
|
|
58
|
+
- \`response_format\` (optional): "text" for formatted output or "json" for raw SearXNG-shaped JSON (may include a \`warnings\` array for non-fatal issues)
|
|
50
59
|
|
|
51
|
-
|
|
60
|
+
Text output can include metadata sections for direct answers, spelling corrections, suggestions, and infoboxes before the result list. JSON output preserves the SearXNG response shape with filtered and sliced \`results\`, and may include a \`warnings\` array for non-fatal issues. Unknown categories or engines are rejected when live \`/config\` validation is available; if \`/config\` is unavailable, the search proceeds with the supplied values and emits a warning.
|
|
61
|
+
|
|
62
|
+
### 2. searxng_search_suggestions
|
|
63
|
+
Returns autocomplete suggestions from the configured SearXNG instance.
|
|
64
|
+
|
|
65
|
+
**Parameters:**
|
|
66
|
+
- \`query\` (required): Partial or complete query to autocomplete
|
|
67
|
+
- \`language\` (optional): Language code like "en", "fr", "de" or "all" (default: "all")
|
|
68
|
+
|
|
69
|
+
### 3. searxng_instance_info
|
|
70
|
+
Discovers categories, engines, defaults, locales, and plugins exposed by the configured SearXNG instance.
|
|
71
|
+
|
|
72
|
+
**Parameters:**
|
|
73
|
+
- \`includeEngines\` (optional): Include enabled engine names
|
|
74
|
+
- \`includeDisabled\` (optional): Include disabled engine names when \`includeEngines\` is true
|
|
75
|
+
- \`category\` (optional): Filter categories and engines to one category
|
|
76
|
+
- \`refresh\` (optional): Bypass the process cache and fetch fresh \`/config\` data
|
|
77
|
+
|
|
78
|
+
### 4. web_url_read
|
|
52
79
|
Reads and converts web page content to Markdown format.
|
|
53
80
|
|
|
54
81
|
**Parameters:**
|
|
55
82
|
- \`url\` (required): The URL to fetch and convert
|
|
83
|
+
- \`startChar\` (optional): Starting character position
|
|
84
|
+
- \`maxLength\` (optional): Maximum number of characters to return
|
|
85
|
+
- \`section\` (optional): Extract content under a heading
|
|
86
|
+
- \`paragraphRange\` (optional): Return a paragraph range such as "1-5" or "10-"
|
|
87
|
+
- \`readHeadings\` (optional): Return only headings
|
|
56
88
|
|
|
57
89
|
## Configuration
|
|
58
90
|
|
|
@@ -98,6 +130,18 @@ Tool: web_url_read
|
|
|
98
130
|
Args: {"url": "https://example.com/article"}
|
|
99
131
|
\`\`\`
|
|
100
132
|
|
|
133
|
+
### Get query suggestions
|
|
134
|
+
\`\`\`
|
|
135
|
+
Tool: searxng_search_suggestions
|
|
136
|
+
Args: {"query": "typescr"}
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
### Discover instance capabilities
|
|
140
|
+
\`\`\`
|
|
141
|
+
Tool: searxng_instance_info
|
|
142
|
+
Args: {"includeEngines": true}
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
101
145
|
## Troubleshooting
|
|
102
146
|
|
|
103
147
|
1. **"SEARXNG_URL not set"**: Configure the SEARXNG_URL environment variable
|
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, num_results?: number, categories?: string): 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, engines?: string, response_format?: "text" | "json"): Promise<string>;
|
package/dist/search.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getKnownCategories, getKnownEngines } from "./instance-info.js";
|
|
1
2
|
import { createProxyAgent, createDefaultAgent, ProxyType } from "./proxy.js";
|
|
2
3
|
import { logMessage } from "./logging.js";
|
|
3
4
|
import { MCPSearXNGError, validateEnvironment, createNetworkError, createServerError, createJSONError, createDataError, createNoResultsMessage } from "./error-handler.js";
|
|
@@ -31,6 +32,158 @@ function truncateResultContent(content, maxResultChars) {
|
|
|
31
32
|
}
|
|
32
33
|
return `${content.slice(0, maxResultChars)}…`;
|
|
33
34
|
}
|
|
35
|
+
function hasItems(items) {
|
|
36
|
+
return Array.isArray(items) && items.length > 0;
|
|
37
|
+
}
|
|
38
|
+
function splitCommaSeparated(value) {
|
|
39
|
+
return value
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((entry) => entry.trim())
|
|
42
|
+
.filter((entry) => entry !== "");
|
|
43
|
+
}
|
|
44
|
+
function buildCanonicalLookup(knownValues) {
|
|
45
|
+
const lookup = new Map();
|
|
46
|
+
for (const value of knownValues) {
|
|
47
|
+
lookup.set(value.trim().toLowerCase(), value);
|
|
48
|
+
}
|
|
49
|
+
return lookup;
|
|
50
|
+
}
|
|
51
|
+
function normalizeCommaSeparated(value, knownValues) {
|
|
52
|
+
const lookup = buildCanonicalLookup(knownValues);
|
|
53
|
+
const normalized = [];
|
|
54
|
+
const invalid = [];
|
|
55
|
+
for (const requested of splitCommaSeparated(value)) {
|
|
56
|
+
const canonical = lookup.get(requested.toLowerCase());
|
|
57
|
+
if (canonical === undefined) {
|
|
58
|
+
invalid.push(requested);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
normalized.push(canonical);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
normalized: normalized.join(","),
|
|
66
|
+
invalid,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function formatAvailableValues(label, values) {
|
|
70
|
+
const available = [...values].sort().join(", ");
|
|
71
|
+
return available === "" ? "" : ` Available ${label}: ${available}.`;
|
|
72
|
+
}
|
|
73
|
+
function createValidationError(kind, invalid, available) {
|
|
74
|
+
const label = kind === "category" ? "categories" : "engines";
|
|
75
|
+
return new MCPSearXNGError(`🔍 Invalid SearXNG ${kind} name(s): ${invalid.join(", ")}.` +
|
|
76
|
+
formatAvailableValues(label, available) +
|
|
77
|
+
` Use the searxng_instance_info tool to discover available ${label}.`);
|
|
78
|
+
}
|
|
79
|
+
async function normalizeSearchFilters(mcpServer, categories, engines) {
|
|
80
|
+
const effectiveCategories = categories !== undefined && categories.trim() !== "" ? categories : undefined;
|
|
81
|
+
const effectiveEngines = engines !== undefined && engines.trim() !== "" ? engines : undefined;
|
|
82
|
+
if (!effectiveCategories && !effectiveEngines) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const unavailableFilterLabel = effectiveCategories && effectiveEngines
|
|
86
|
+
? "categories and engines"
|
|
87
|
+
: effectiveCategories
|
|
88
|
+
? "categories"
|
|
89
|
+
: "engines";
|
|
90
|
+
const unavailableWarning = `${unavailableFilterLabel[0].toUpperCase()}${unavailableFilterLabel.slice(1)} were not validated or normalized because SearXNG /config is unavailable.`;
|
|
91
|
+
const unavailableNote = `Note: ${unavailableFilterLabel} were not validated or normalized (SearXNG /config unavailable).`;
|
|
92
|
+
let knownCategories;
|
|
93
|
+
let knownEngines;
|
|
94
|
+
if (effectiveCategories) {
|
|
95
|
+
knownCategories = await getKnownCategories(mcpServer);
|
|
96
|
+
if (knownCategories === null) {
|
|
97
|
+
return {
|
|
98
|
+
categories: effectiveCategories,
|
|
99
|
+
engines: effectiveEngines,
|
|
100
|
+
validationWarning: unavailableWarning,
|
|
101
|
+
validationNote: unavailableNote,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (effectiveEngines) {
|
|
106
|
+
knownEngines = await getKnownEngines(mcpServer);
|
|
107
|
+
if (knownEngines === null) {
|
|
108
|
+
return {
|
|
109
|
+
categories: effectiveCategories,
|
|
110
|
+
engines: effectiveEngines,
|
|
111
|
+
validationWarning: unavailableWarning,
|
|
112
|
+
validationNote: unavailableNote,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let normalizedCategories = effectiveCategories && knownCategories
|
|
117
|
+
? normalizeCommaSeparated(effectiveCategories, knownCategories)
|
|
118
|
+
: undefined;
|
|
119
|
+
let normalizedEngines = effectiveEngines && knownEngines
|
|
120
|
+
? normalizeCommaSeparated(effectiveEngines, knownEngines)
|
|
121
|
+
: undefined;
|
|
122
|
+
if ((normalizedCategories && normalizedCategories.invalid.length > 0) ||
|
|
123
|
+
(normalizedEngines && normalizedEngines.invalid.length > 0)) {
|
|
124
|
+
if (effectiveCategories) {
|
|
125
|
+
knownCategories = await getKnownCategories(mcpServer, true);
|
|
126
|
+
knownEngines = effectiveEngines && knownCategories !== null
|
|
127
|
+
? await getKnownEngines(mcpServer)
|
|
128
|
+
: knownEngines;
|
|
129
|
+
}
|
|
130
|
+
else if (effectiveEngines) {
|
|
131
|
+
knownEngines = await getKnownEngines(mcpServer, true);
|
|
132
|
+
}
|
|
133
|
+
if (knownCategories === null || knownEngines === null) {
|
|
134
|
+
return {
|
|
135
|
+
categories: effectiveCategories,
|
|
136
|
+
engines: effectiveEngines,
|
|
137
|
+
validationWarning: unavailableWarning,
|
|
138
|
+
validationNote: unavailableNote,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
normalizedCategories = effectiveCategories && knownCategories
|
|
142
|
+
? normalizeCommaSeparated(effectiveCategories, knownCategories)
|
|
143
|
+
: undefined;
|
|
144
|
+
normalizedEngines = effectiveEngines && knownEngines
|
|
145
|
+
? normalizeCommaSeparated(effectiveEngines, knownEngines)
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|
|
148
|
+
if (normalizedCategories && normalizedCategories.invalid.length > 0 && knownCategories) {
|
|
149
|
+
throw createValidationError("category", normalizedCategories.invalid, knownCategories);
|
|
150
|
+
}
|
|
151
|
+
if (normalizedEngines && normalizedEngines.invalid.length > 0 && knownEngines) {
|
|
152
|
+
throw createValidationError("engine", normalizedEngines.invalid, knownEngines);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
categories: normalizedCategories ? normalizedCategories.normalized : undefined,
|
|
156
|
+
engines: normalizedEngines ? normalizedEngines.normalized : undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function formatSearchMetadata(data) {
|
|
160
|
+
const sections = [];
|
|
161
|
+
if (hasItems(data.answers)) {
|
|
162
|
+
sections.push(data.answers.map((answer) => `Direct answer: ${answer}`).join("\n"));
|
|
163
|
+
}
|
|
164
|
+
if (hasItems(data.corrections)) {
|
|
165
|
+
sections.push(data.corrections.map((correction) => `Spelling correction: did you mean "${correction}"?`).join("\n"));
|
|
166
|
+
}
|
|
167
|
+
if (hasItems(data.suggestions)) {
|
|
168
|
+
sections.push(`Suggestions: ${data.suggestions.join(", ")}`);
|
|
169
|
+
}
|
|
170
|
+
if (hasItems(data.infoboxes)) {
|
|
171
|
+
const infoboxText = data.infoboxes
|
|
172
|
+
.map((infobox) => {
|
|
173
|
+
const lines = [`Infobox: ${infobox.infobox}`];
|
|
174
|
+
if (infobox.content) {
|
|
175
|
+
lines.push(infobox.content);
|
|
176
|
+
}
|
|
177
|
+
if (hasItems(infobox.urls)) {
|
|
178
|
+
lines.push(...infobox.urls.map((entry) => `${entry.title}: ${entry.url}`));
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
})
|
|
182
|
+
.join("\n\n");
|
|
183
|
+
sections.push(infoboxText);
|
|
184
|
+
}
|
|
185
|
+
return sections.join("\n\n");
|
|
186
|
+
}
|
|
34
187
|
function getDefaultLanguage() {
|
|
35
188
|
return process.env.SEARXNG_DEFAULT_LANGUAGE ?? "all";
|
|
36
189
|
}
|
|
@@ -46,7 +199,7 @@ function getDefaultSafesearch(mcpServer) {
|
|
|
46
199
|
}
|
|
47
200
|
return parsed;
|
|
48
201
|
}
|
|
49
|
-
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories) {
|
|
202
|
+
export async function performWebSearch(mcpServer, query, pageno = 1, time_range, language, safesearch, min_score, num_results, categories, engines, response_format = "text") {
|
|
50
203
|
const startTime = Date.now();
|
|
51
204
|
const operatorMax = getOperatorMaxResults(mcpServer);
|
|
52
205
|
const effectiveMax = operatorMax !== undefined
|
|
@@ -55,6 +208,12 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
55
208
|
const maxResultChars = getMaxResultChars(mcpServer);
|
|
56
209
|
const effectiveLanguage = language ?? getDefaultLanguage();
|
|
57
210
|
const effectiveSafesearch = safesearch !== undefined ? safesearch : getDefaultSafesearch(mcpServer);
|
|
211
|
+
const validationError = validateEnvironment();
|
|
212
|
+
if (validationError) {
|
|
213
|
+
logMessage(mcpServer, "error", "Configuration invalid");
|
|
214
|
+
throw new MCPSearXNGError(validationError);
|
|
215
|
+
}
|
|
216
|
+
const filters = await normalizeSearchFilters(mcpServer, categories, engines);
|
|
58
217
|
// Build detailed log message with all parameters
|
|
59
218
|
const searchParams = [
|
|
60
219
|
`page ${pageno}`,
|
|
@@ -63,14 +222,10 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
63
222
|
effectiveSafesearch !== undefined ? `safesearch: ${effectiveSafesearch}` : null,
|
|
64
223
|
min_score !== undefined ? `min_score: ${min_score}` : null,
|
|
65
224
|
effectiveMax !== undefined ? `num_results: ${effectiveMax}` : null,
|
|
66
|
-
categories ? `categories: ${categories}` : null,
|
|
225
|
+
filters.categories ? `categories: ${filters.categories}` : null,
|
|
226
|
+
filters.engines ? `engines: ${filters.engines}` : null,
|
|
67
227
|
].filter(Boolean).join(", ");
|
|
68
228
|
logMessage(mcpServer, "info", `Starting web search: "${query}" (${searchParams})`);
|
|
69
|
-
const validationError = validateEnvironment();
|
|
70
|
-
if (validationError) {
|
|
71
|
-
logMessage(mcpServer, "error", "Configuration invalid");
|
|
72
|
-
throw new MCPSearXNGError(validationError);
|
|
73
|
-
}
|
|
74
229
|
const searxngUrl = process.env.SEARXNG_URL;
|
|
75
230
|
const parsedUrl = new URL(searxngUrl.endsWith('/') ? searxngUrl : searxngUrl + '/');
|
|
76
231
|
const url = new URL('search', parsedUrl);
|
|
@@ -87,8 +242,11 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
87
242
|
if (effectiveSafesearch !== undefined && [0, 1, 2].includes(effectiveSafesearch)) {
|
|
88
243
|
url.searchParams.set("safesearch", effectiveSafesearch.toString());
|
|
89
244
|
}
|
|
90
|
-
if (categories) {
|
|
91
|
-
url.searchParams.set("categories", categories);
|
|
245
|
+
if (filters.categories) {
|
|
246
|
+
url.searchParams.set("categories", filters.categories);
|
|
247
|
+
}
|
|
248
|
+
if (filters.engines) {
|
|
249
|
+
url.searchParams.set("engines", filters.engines);
|
|
92
250
|
}
|
|
93
251
|
// Prepare request options with headers
|
|
94
252
|
const requestOptions = {
|
|
@@ -177,16 +335,22 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
177
335
|
throw createDataError(data, context);
|
|
178
336
|
}
|
|
179
337
|
const results = data.results
|
|
180
|
-
.
|
|
181
|
-
title: result.title || "",
|
|
182
|
-
content: result.content || "",
|
|
183
|
-
url: result.url || "",
|
|
184
|
-
score: result.score || 0,
|
|
185
|
-
}))
|
|
186
|
-
.filter((result) => min_score === undefined || result.score >= min_score);
|
|
338
|
+
.filter((result) => min_score === undefined || (result.score || 0) >= min_score);
|
|
187
339
|
const slicedResults = effectiveMax !== undefined
|
|
188
340
|
? results.slice(0, effectiveMax)
|
|
189
341
|
: results;
|
|
342
|
+
if (response_format === "json") {
|
|
343
|
+
return JSON.stringify({
|
|
344
|
+
...data,
|
|
345
|
+
results: slicedResults,
|
|
346
|
+
...(filters.validationWarning ? { warnings: [filters.validationWarning] } : {}),
|
|
347
|
+
}, null, 2);
|
|
348
|
+
}
|
|
349
|
+
const metadata = formatSearchMetadata(data);
|
|
350
|
+
const leadingSections = [
|
|
351
|
+
filters.validationNote ?? null,
|
|
352
|
+
metadata || null,
|
|
353
|
+
].filter(Boolean).join("\n\n");
|
|
190
354
|
if (slicedResults.length === 0) {
|
|
191
355
|
const appliedFilters = [
|
|
192
356
|
min_score === undefined ? null : `min_score=${min_score}`,
|
|
@@ -194,11 +358,16 @@ export async function performWebSearch(mcpServer, query, pageno = 1, time_range,
|
|
|
194
358
|
].filter(Boolean).join(" ");
|
|
195
359
|
const filterNote = appliedFilters ? ` after applying ${appliedFilters}` : "";
|
|
196
360
|
logMessage(mcpServer, "info", `No results found for query: "${query}"${filterNote}`);
|
|
197
|
-
|
|
361
|
+
const noResultsMessage = createNoResultsMessage(query);
|
|
362
|
+
return leadingSections ? `${leadingSections}\n\n---\n\n${noResultsMessage}` : noResultsMessage;
|
|
198
363
|
}
|
|
199
364
|
const duration = Date.now() - startTime;
|
|
200
365
|
logMessage(mcpServer, "info", `Search completed: "${query}" (${searchParams}) - ${slicedResults.length} results in ${duration}ms`);
|
|
201
|
-
|
|
202
|
-
.map((r) =>
|
|
366
|
+
const formattedResults = slicedResults
|
|
367
|
+
.map((r) => {
|
|
368
|
+
const score = r.score || 0;
|
|
369
|
+
return `Title: ${r.title || ""}\nDescription: ${truncateResultContent(r.content || "", maxResultChars)}\nURL: ${r.url || ""}\nRelevance Score: ${score.toFixed(3)}`;
|
|
370
|
+
})
|
|
203
371
|
.join("\n\n");
|
|
372
|
+
return leadingSections ? `${leadingSections}\n\n---\n\n${formattedResults}` : formattedResults;
|
|
204
373
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { logMessage } from "./logging.js";
|
|
2
|
+
import { createDefaultAgent, createProxyAgent, ProxyType } from "./proxy.js";
|
|
3
|
+
export async function performSearchSuggestions(mcpServer, query, language = "all") {
|
|
4
|
+
const base = process.env.SEARXNG_URL;
|
|
5
|
+
if (!base) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const parsedBase = new URL(base.endsWith("/") ? base : `${base}/`);
|
|
9
|
+
const url = new URL("autocompleter", parsedBase);
|
|
10
|
+
url.searchParams.set("q", query);
|
|
11
|
+
if (language !== "all") {
|
|
12
|
+
url.searchParams.set("lang", language);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const requestOptions = {
|
|
16
|
+
signal: AbortSignal.timeout(5000),
|
|
17
|
+
};
|
|
18
|
+
const proxyAgent = createProxyAgent(url.toString(), ProxyType.SEARCH);
|
|
19
|
+
const dispatcher = proxyAgent ?? createDefaultAgent();
|
|
20
|
+
if (dispatcher) {
|
|
21
|
+
requestOptions.dispatcher = dispatcher;
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(url.toString(), requestOptions);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return Array.isArray(data[1]) ? data[1] : [];
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
logMessage(mcpServer, "debug", "Autocomplete request failed; returning empty suggestions");
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -38,8 +38,24 @@ export declare function isSearXNGWebSearchArgs(args: unknown): args is {
|
|
|
38
38
|
min_score?: number;
|
|
39
39
|
num_results?: number;
|
|
40
40
|
categories?: string;
|
|
41
|
+
engines?: string;
|
|
42
|
+
response_format?: "text" | "json";
|
|
43
|
+
};
|
|
44
|
+
export declare function isSearXNGSearchSuggestionsArgs(args: unknown): args is {
|
|
45
|
+
query: string;
|
|
46
|
+
language?: string;
|
|
47
|
+
};
|
|
48
|
+
export declare function isSearXNGInstanceInfoArgs(args: unknown): args is {
|
|
49
|
+
includeEngines?: boolean;
|
|
50
|
+
includeDisabled?: boolean;
|
|
51
|
+
category?: string;
|
|
52
|
+
refresh?: boolean;
|
|
41
53
|
};
|
|
42
54
|
export declare const WEB_SEARCH_TOOL: Tool;
|
|
55
|
+
export declare const SUGGESTIONS_TOOL: Tool;
|
|
56
|
+
export declare const INSTANCE_INFO_TOOL: Tool;
|
|
43
57
|
export declare const LITE_WEB_SEARCH_TOOL: Tool;
|
|
58
|
+
export declare const LITE_SUGGESTIONS_TOOL: Tool;
|
|
59
|
+
export declare const LITE_INSTANCE_INFO_TOOL: Tool;
|
|
44
60
|
export declare const LITE_READ_URL_TOOL: Tool;
|
|
45
61
|
export declare const READ_URL_TOOL: Tool;
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const VALID_TIME_RANGES = ["day", "week", "month", "year"];
|
|
2
2
|
const VALID_SAFESEARCH_VALUES = [0, 1, 2];
|
|
3
|
+
const VALID_RESPONSE_FORMATS = ["text", "json"];
|
|
3
4
|
export function isSearXNGWebSearchArgs(args) {
|
|
4
5
|
if (typeof args !== "object" ||
|
|
5
6
|
args === null ||
|
|
@@ -40,6 +41,45 @@ export function isSearXNGWebSearchArgs(args) {
|
|
|
40
41
|
if (searchArgs.categories !== undefined && typeof searchArgs.categories !== "string") {
|
|
41
42
|
return false;
|
|
42
43
|
}
|
|
44
|
+
if (searchArgs.engines !== undefined && typeof searchArgs.engines !== "string") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (searchArgs.response_format !== undefined &&
|
|
48
|
+
(typeof searchArgs.response_format !== "string" || !VALID_RESPONSE_FORMATS.includes(searchArgs.response_format))) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function isSearXNGSearchSuggestionsArgs(args) {
|
|
54
|
+
if (typeof args !== "object" ||
|
|
55
|
+
args === null ||
|
|
56
|
+
!("query" in args) ||
|
|
57
|
+
typeof args.query !== "string") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const suggestionArgs = args;
|
|
61
|
+
if (suggestionArgs.language !== undefined && typeof suggestionArgs.language !== "string") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
export function isSearXNGInstanceInfoArgs(args) {
|
|
67
|
+
if (typeof args !== "object" || args === null) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const infoArgs = args;
|
|
71
|
+
if (infoArgs.includeEngines !== undefined && typeof infoArgs.includeEngines !== "boolean") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (infoArgs.includeDisabled !== undefined && typeof infoArgs.includeDisabled !== "boolean") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (infoArgs.category !== undefined && typeof infoArgs.category !== "string") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (infoArgs.refresh !== undefined && typeof infoArgs.refresh !== "boolean") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
43
83
|
return true;
|
|
44
84
|
}
|
|
45
85
|
export const WEB_SEARCH_TOOL = {
|
|
@@ -95,12 +135,79 @@ export const WEB_SEARCH_TOOL = {
|
|
|
95
135
|
},
|
|
96
136
|
categories: {
|
|
97
137
|
type: "string",
|
|
98
|
-
description: "Comma-separated SearXNG categories.
|
|
138
|
+
description: "Comma-separated SearXNG categories. Values are normalized case-insensitively to canonical names from live /config; unknown values are rejected with available categories listed. If /config is unavailable, values are forwarded as-is with a warning.",
|
|
139
|
+
},
|
|
140
|
+
engines: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Comma-separated SearXNG engine names to query (e.g. 'google,bing,ddg'). Values are normalized case-insensitively to canonical names from live /config; unknown values are rejected with available engines listed. If /config is unavailable, values are forwarded as-is with a warning.",
|
|
143
|
+
},
|
|
144
|
+
response_format: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "Response format: formatted text for agents or raw JSON for programmatic clients. Default: text.",
|
|
147
|
+
enum: ["text", "json"],
|
|
148
|
+
default: "text",
|
|
99
149
|
},
|
|
100
150
|
},
|
|
101
151
|
required: ["query"],
|
|
102
152
|
},
|
|
103
153
|
};
|
|
154
|
+
export const SUGGESTIONS_TOOL = {
|
|
155
|
+
name: "searxng_search_suggestions",
|
|
156
|
+
description: "Returns autocomplete suggestions from the configured SearXNG instance. " +
|
|
157
|
+
"Use this to refine vague or partial queries before searching.",
|
|
158
|
+
annotations: {
|
|
159
|
+
readOnlyHint: true,
|
|
160
|
+
openWorldHint: true,
|
|
161
|
+
},
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
query: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "Partial or complete query to autocomplete.",
|
|
168
|
+
},
|
|
169
|
+
language: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Language code for suggestions (e.g., 'en', 'fr', 'de') or 'all'. Default: all.",
|
|
172
|
+
default: "all",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ["query"],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
export const INSTANCE_INFO_TOOL = {
|
|
179
|
+
name: "searxng_instance_info",
|
|
180
|
+
description: "Discovers capabilities from the configured SearXNG instance via /config, including categories, engines, defaults, locales, and plugins.",
|
|
181
|
+
annotations: {
|
|
182
|
+
readOnlyHint: true,
|
|
183
|
+
openWorldHint: true,
|
|
184
|
+
},
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
includeEngines: {
|
|
189
|
+
type: "boolean",
|
|
190
|
+
description: "Include enabled engine names in the response.",
|
|
191
|
+
default: false,
|
|
192
|
+
},
|
|
193
|
+
includeDisabled: {
|
|
194
|
+
type: "boolean",
|
|
195
|
+
description: "Include disabled engine names when includeEngines is true.",
|
|
196
|
+
default: false,
|
|
197
|
+
},
|
|
198
|
+
category: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "Filter categories and engines to a single category name.",
|
|
201
|
+
},
|
|
202
|
+
refresh: {
|
|
203
|
+
type: "boolean",
|
|
204
|
+
description: "Bypass the process cache and fetch fresh /config data.",
|
|
205
|
+
default: false,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: [],
|
|
209
|
+
},
|
|
210
|
+
};
|
|
104
211
|
export const LITE_WEB_SEARCH_TOOL = {
|
|
105
212
|
name: "searxng_web_search",
|
|
106
213
|
description: "Web search. Returns titles, URLs, snippets.",
|
|
@@ -110,6 +217,24 @@ export const LITE_WEB_SEARCH_TOOL = {
|
|
|
110
217
|
required: ["query"],
|
|
111
218
|
},
|
|
112
219
|
};
|
|
220
|
+
export const LITE_SUGGESTIONS_TOOL = {
|
|
221
|
+
name: "searxng_search_suggestions",
|
|
222
|
+
description: "Autocomplete search query suggestions.",
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: { query: { type: "string", description: "Query prefix." } },
|
|
226
|
+
required: ["query"],
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
export const LITE_INSTANCE_INFO_TOOL = {
|
|
230
|
+
name: "searxng_instance_info",
|
|
231
|
+
description: "Discover SearXNG instance capabilities.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {},
|
|
235
|
+
required: [],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
113
238
|
export const LITE_READ_URL_TOOL = {
|
|
114
239
|
name: "web_url_read",
|
|
115
240
|
description: "Fetch URL. Returns page text as markdown.",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const packageVersion = "1.
|
|
1
|
+
export declare const packageVersion = "1.6.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.
|
|
1
|
+
export const packageVersion = "1.6.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-searxng",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
|
|
5
5
|
"description": "MCP server for SearXNG integration",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,5 +71,8 @@
|
|
|
71
71
|
"supertest": "^7.2.2",
|
|
72
72
|
"tsx": "4.22.4",
|
|
73
73
|
"typescript": "^5.8.3"
|
|
74
|
+
},
|
|
75
|
+
"overrides": {
|
|
76
|
+
"hono": ">=4.12.25"
|
|
74
77
|
}
|
|
75
78
|
}
|