@amplify-studio/open-mcp 0.8.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/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.js +68 -0
- package/dist/error-handler.d.ts +29 -0
- package/dist/error-handler.js +114 -0
- package/dist/http-server.d.ts +3 -0
- package/dist/http-server.js +150 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +253 -0
- package/dist/logging.d.ts +6 -0
- package/dist/logging.js +42 -0
- package/dist/proxy.d.ts +16 -0
- package/dist/proxy.js +97 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +95 -0
- package/dist/search.d.ts +2 -0
- package/dist/search.js +130 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +79 -0
- package/dist/url-reader.d.ts +10 -0
- package/dist/url-reader.js +212 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
// Import modularized functionality
|
|
6
|
+
import { WEB_SEARCH_TOOL, READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
|
|
7
|
+
import { logMessage, setLogLevel } from "./logging.js";
|
|
8
|
+
import { performWebSearch } from "./search.js";
|
|
9
|
+
import { fetchAndConvertToMarkdown } from "./url-reader.js";
|
|
10
|
+
import { createConfigResource, createHelpResource } from "./resources.js";
|
|
11
|
+
import { createHttpServer } from "./http-server.js";
|
|
12
|
+
import { validateEnvironment as validateEnv } from "./error-handler.js";
|
|
13
|
+
// Use a static version string that will be updated by the version script
|
|
14
|
+
const packageVersion = "0.8.0";
|
|
15
|
+
// Export the version for use in other modules
|
|
16
|
+
export { packageVersion };
|
|
17
|
+
// Global state for logging level
|
|
18
|
+
let currentLogLevel = "info";
|
|
19
|
+
// Type guard for URL reading args
|
|
20
|
+
export function isWebUrlReadArgs(args) {
|
|
21
|
+
if (typeof args !== "object" ||
|
|
22
|
+
args === null ||
|
|
23
|
+
!("url" in args) ||
|
|
24
|
+
typeof args.url !== "string") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const urlArgs = args;
|
|
28
|
+
// Convert empty strings to undefined for optional string parameters
|
|
29
|
+
if (urlArgs.section === "")
|
|
30
|
+
urlArgs.section = undefined;
|
|
31
|
+
if (urlArgs.paragraphRange === "")
|
|
32
|
+
urlArgs.paragraphRange = undefined;
|
|
33
|
+
// Validate optional parameters
|
|
34
|
+
if (urlArgs.startChar !== undefined && (typeof urlArgs.startChar !== "number" || urlArgs.startChar < 0)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (urlArgs.maxLength !== undefined && (typeof urlArgs.maxLength !== "number" || urlArgs.maxLength < 1)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (urlArgs.section !== undefined && typeof urlArgs.section !== "string") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (urlArgs.paragraphRange !== undefined && typeof urlArgs.paragraphRange !== "string") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (urlArgs.readHeadings !== undefined && typeof urlArgs.readHeadings !== "boolean") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Server implementation
|
|
52
|
+
const server = new Server({
|
|
53
|
+
name: "ihor-sokoliuk/mcp-searxng",
|
|
54
|
+
version: packageVersion,
|
|
55
|
+
}, {
|
|
56
|
+
capabilities: {
|
|
57
|
+
logging: {},
|
|
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
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// List tools handler
|
|
72
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
73
|
+
logMessage(server, "debug", "Handling list_tools request");
|
|
74
|
+
return {
|
|
75
|
+
tools: [WEB_SEARCH_TOOL, READ_URL_TOOL],
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
// Call tool handler
|
|
79
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
80
|
+
const { name, arguments: args } = request.params;
|
|
81
|
+
logMessage(server, "debug", `Handling call_tool request: ${name}`);
|
|
82
|
+
try {
|
|
83
|
+
if (name === "searxng_web_search") {
|
|
84
|
+
if (!isSearXNGWebSearchArgs(args)) {
|
|
85
|
+
throw new Error("Invalid arguments for web search");
|
|
86
|
+
}
|
|
87
|
+
const result = await performWebSearch(server, args.query, args.pageno, args.time_range, args.language, args.safesearch);
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: result,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else if (name === "web_url_read") {
|
|
98
|
+
if (!isWebUrlReadArgs(args)) {
|
|
99
|
+
throw new Error("Invalid arguments for URL reading");
|
|
100
|
+
}
|
|
101
|
+
const paginationOptions = {
|
|
102
|
+
startChar: args.startChar,
|
|
103
|
+
maxLength: args.maxLength,
|
|
104
|
+
section: args.section,
|
|
105
|
+
paragraphRange: args.paragraphRange,
|
|
106
|
+
readHeadings: args.readHeadings,
|
|
107
|
+
};
|
|
108
|
+
const result = await fetchAndConvertToMarkdown(server, args.url, 10000, paginationOptions);
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: result,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logMessage(server, "error", `Tool execution error: ${error instanceof Error ? error.message : String(error)}`, {
|
|
124
|
+
tool: name,
|
|
125
|
+
args: args,
|
|
126
|
+
error: error instanceof Error ? error.stack : String(error)
|
|
127
|
+
});
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Logging level handler
|
|
132
|
+
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
133
|
+
const { level } = request.params;
|
|
134
|
+
logMessage(server, "info", `Setting log level to: ${level}`);
|
|
135
|
+
currentLogLevel = level;
|
|
136
|
+
setLogLevel(level);
|
|
137
|
+
return {};
|
|
138
|
+
});
|
|
139
|
+
// List resources handler
|
|
140
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
141
|
+
logMessage(server, "debug", "Handling list_resources request");
|
|
142
|
+
return {
|
|
143
|
+
resources: [
|
|
144
|
+
{
|
|
145
|
+
uri: "config://server-config",
|
|
146
|
+
mimeType: "application/json",
|
|
147
|
+
name: "Server Configuration",
|
|
148
|
+
description: "Current server configuration and environment variables"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
uri: "help://usage-guide",
|
|
152
|
+
mimeType: "text/markdown",
|
|
153
|
+
name: "Usage Guide",
|
|
154
|
+
description: "How to use the MCP SearXNG server effectively"
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
// Read resource handler
|
|
160
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
161
|
+
const { uri } = request.params;
|
|
162
|
+
logMessage(server, "debug", `Handling read_resource request for: ${uri}`);
|
|
163
|
+
switch (uri) {
|
|
164
|
+
case "config://server-config":
|
|
165
|
+
return {
|
|
166
|
+
contents: [
|
|
167
|
+
{
|
|
168
|
+
uri: uri,
|
|
169
|
+
mimeType: "application/json",
|
|
170
|
+
text: createConfigResource()
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
case "help://usage-guide":
|
|
175
|
+
return {
|
|
176
|
+
contents: [
|
|
177
|
+
{
|
|
178
|
+
uri: uri,
|
|
179
|
+
mimeType: "text/markdown",
|
|
180
|
+
text: createHelpResource()
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// Main function
|
|
189
|
+
async function main() {
|
|
190
|
+
// Environment validation
|
|
191
|
+
const validationError = validateEnv();
|
|
192
|
+
if (validationError) {
|
|
193
|
+
console.error(`❌ ${validationError}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// Check for HTTP transport mode
|
|
197
|
+
const httpPort = process.env.MCP_HTTP_PORT;
|
|
198
|
+
if (httpPort) {
|
|
199
|
+
const port = parseInt(httpPort, 10);
|
|
200
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
201
|
+
console.error(`Invalid HTTP port: ${httpPort}. Must be between 1-65535.`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.log(`Starting HTTP transport on port ${port}`);
|
|
205
|
+
const app = await createHttpServer(server);
|
|
206
|
+
const httpServer = app.listen(port, () => {
|
|
207
|
+
console.log(`HTTP server listening on port ${port}`);
|
|
208
|
+
console.log(`Health check: http://localhost:${port}/health`);
|
|
209
|
+
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
|
210
|
+
});
|
|
211
|
+
// Handle graceful shutdown
|
|
212
|
+
const shutdown = (signal) => {
|
|
213
|
+
console.log(`Received ${signal}. Shutting down HTTP server...`);
|
|
214
|
+
httpServer.close(() => {
|
|
215
|
+
console.log("HTTP server closed");
|
|
216
|
+
process.exit(0);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
220
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Default STDIO transport
|
|
224
|
+
// Show helpful message when running in terminal
|
|
225
|
+
if (process.stdin.isTTY) {
|
|
226
|
+
console.log(`🔍 MCP SearXNG Server v${packageVersion} - Ready`);
|
|
227
|
+
console.log("✅ Configuration valid");
|
|
228
|
+
console.log(`🌐 Gateway URL: ${process.env.GATEWAY_URL || "http://115.190.91.253:80 (default)"}`);
|
|
229
|
+
console.log("📡 Waiting for MCP client connection via STDIO...\n");
|
|
230
|
+
}
|
|
231
|
+
const transport = new StdioServerTransport();
|
|
232
|
+
await server.connect(transport);
|
|
233
|
+
// Log after connection is established
|
|
234
|
+
logMessage(server, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
|
|
235
|
+
logMessage(server, "info", `Log level: ${currentLogLevel}`);
|
|
236
|
+
logMessage(server, "info", `Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
237
|
+
logMessage(server, "info", `Gateway URL: ${process.env.GATEWAY_URL || 'http://115.190.91.253:80 (default)'}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Handle uncaught errors
|
|
241
|
+
process.on('uncaughtException', (error) => {
|
|
242
|
+
console.error('Uncaught Exception:', error);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
245
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
246
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|
|
249
|
+
// Start the server (CLI entrypoint)
|
|
250
|
+
main().catch((error) => {
|
|
251
|
+
console.error("Failed to start server:", error);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare function logMessage(server: Server, level: LoggingLevel, message: string, data?: unknown): void;
|
|
4
|
+
export declare function shouldLog(level: LoggingLevel): boolean;
|
|
5
|
+
export declare function setLogLevel(level: LoggingLevel): void;
|
|
6
|
+
export declare function getCurrentLogLevel(): LoggingLevel;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Logging state
|
|
2
|
+
let currentLogLevel = "info";
|
|
3
|
+
// Logging helper function
|
|
4
|
+
export function logMessage(server, level, message, data) {
|
|
5
|
+
if (shouldLog(level)) {
|
|
6
|
+
try {
|
|
7
|
+
// Merge message and data together for the notification body
|
|
8
|
+
const notificationData = data !== undefined
|
|
9
|
+
? (typeof data === 'object' && data !== null ? { message, ...data } : { message, data })
|
|
10
|
+
: { message };
|
|
11
|
+
server.notification({
|
|
12
|
+
method: "notifications/message",
|
|
13
|
+
params: {
|
|
14
|
+
level,
|
|
15
|
+
data: notificationData
|
|
16
|
+
}
|
|
17
|
+
}).catch((error) => {
|
|
18
|
+
// Silently ignore "Not connected" errors during server startup
|
|
19
|
+
// This can happen when logging occurs before the transport is fully connected
|
|
20
|
+
if (error instanceof Error && error.message !== "Not connected") {
|
|
21
|
+
console.error("Logging error:", error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Handle synchronous errors as well
|
|
27
|
+
if (error instanceof Error && error.message !== "Not connected") {
|
|
28
|
+
console.error("Logging error:", error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function shouldLog(level) {
|
|
34
|
+
const levels = ["debug", "info", "warning", "error"];
|
|
35
|
+
return levels.indexOf(level) >= levels.indexOf(currentLogLevel);
|
|
36
|
+
}
|
|
37
|
+
export function setLogLevel(level) {
|
|
38
|
+
currentLogLevel = level;
|
|
39
|
+
}
|
|
40
|
+
export function getCurrentLogLevel() {
|
|
41
|
+
return currentLogLevel;
|
|
42
|
+
}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ProxyAgent } from "undici";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a proxy agent dispatcher for Node.js fetch API.
|
|
4
|
+
*
|
|
5
|
+
* Node.js fetch uses Undici under the hood, which requires a 'dispatcher' option
|
|
6
|
+
* instead of 'agent'. This function creates a ProxyAgent compatible with fetch.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables checked (in order):
|
|
9
|
+
* - HTTP_PROXY / http_proxy: For HTTP requests
|
|
10
|
+
* - HTTPS_PROXY / https_proxy: For HTTPS requests
|
|
11
|
+
* - NO_PROXY / no_proxy: Comma-separated list of hosts to bypass proxy
|
|
12
|
+
*
|
|
13
|
+
* @param targetUrl - Optional target URL to check against NO_PROXY rules
|
|
14
|
+
* @returns ProxyAgent dispatcher for fetch, or undefined if no proxy configured or bypassed
|
|
15
|
+
*/
|
|
16
|
+
export declare function createProxyAgent(targetUrl?: string): ProxyAgent | undefined;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ProxyAgent } from "undici";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a target URL should bypass the proxy based on NO_PROXY environment variable.
|
|
4
|
+
*
|
|
5
|
+
* @param targetUrl - The URL to check against NO_PROXY rules
|
|
6
|
+
* @returns true if the URL should bypass the proxy, false otherwise
|
|
7
|
+
*/
|
|
8
|
+
function shouldBypassProxy(targetUrl) {
|
|
9
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
|
10
|
+
if (!noProxy) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
// Wildcard bypass
|
|
14
|
+
if (noProxy.trim() === '*') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
let hostname;
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(targetUrl);
|
|
20
|
+
hostname = url.hostname.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// Invalid URL, don't bypass
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Parse comma-separated list of bypass patterns
|
|
27
|
+
const bypassPatterns = noProxy.split(',').map(pattern => pattern.trim().toLowerCase());
|
|
28
|
+
for (const pattern of bypassPatterns) {
|
|
29
|
+
if (!pattern)
|
|
30
|
+
continue;
|
|
31
|
+
// Exact hostname match
|
|
32
|
+
if (hostname === pattern) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Domain suffix match with leading dot (e.g., .example.com matches sub.example.com)
|
|
36
|
+
if (pattern.startsWith('.') && hostname.endsWith(pattern)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// Domain suffix match without leading dot (e.g., example.com matches sub.example.com and example.com)
|
|
40
|
+
if (!pattern.startsWith('.')) {
|
|
41
|
+
// Exact match
|
|
42
|
+
if (hostname === pattern) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Subdomain match
|
|
46
|
+
if (hostname.endsWith(`.${pattern}`)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a proxy agent dispatcher for Node.js fetch API.
|
|
55
|
+
*
|
|
56
|
+
* Node.js fetch uses Undici under the hood, which requires a 'dispatcher' option
|
|
57
|
+
* instead of 'agent'. This function creates a ProxyAgent compatible with fetch.
|
|
58
|
+
*
|
|
59
|
+
* Environment variables checked (in order):
|
|
60
|
+
* - HTTP_PROXY / http_proxy: For HTTP requests
|
|
61
|
+
* - HTTPS_PROXY / https_proxy: For HTTPS requests
|
|
62
|
+
* - NO_PROXY / no_proxy: Comma-separated list of hosts to bypass proxy
|
|
63
|
+
*
|
|
64
|
+
* @param targetUrl - Optional target URL to check against NO_PROXY rules
|
|
65
|
+
* @returns ProxyAgent dispatcher for fetch, or undefined if no proxy configured or bypassed
|
|
66
|
+
*/
|
|
67
|
+
export function createProxyAgent(targetUrl) {
|
|
68
|
+
const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy;
|
|
69
|
+
if (!proxyUrl) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
// Check if target URL should bypass proxy
|
|
73
|
+
if (targetUrl && shouldBypassProxy(targetUrl)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
// Validate and normalize proxy URL
|
|
77
|
+
let parsedProxyUrl;
|
|
78
|
+
try {
|
|
79
|
+
parsedProxyUrl = new URL(proxyUrl);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new Error(`Invalid proxy URL: ${proxyUrl}. ` +
|
|
83
|
+
"Please provide a valid URL (e.g., http://proxy:8080 or http://user:pass@proxy:8080)");
|
|
84
|
+
}
|
|
85
|
+
// Ensure proxy protocol is supported
|
|
86
|
+
if (!['http:', 'https:'].includes(parsedProxyUrl.protocol)) {
|
|
87
|
+
throw new Error(`Unsupported proxy protocol: ${parsedProxyUrl.protocol}. ` +
|
|
88
|
+
"Only HTTP and HTTPS proxies are supported.");
|
|
89
|
+
}
|
|
90
|
+
// Reconstruct base proxy URL preserving credentials
|
|
91
|
+
const auth = parsedProxyUrl.username ?
|
|
92
|
+
(parsedProxyUrl.password ? `${parsedProxyUrl.username}:${parsedProxyUrl.password}@` : `${parsedProxyUrl.username}@`) :
|
|
93
|
+
'';
|
|
94
|
+
const normalizedProxyUrl = `${parsedProxyUrl.protocol}//${auth}${parsedProxyUrl.host}`;
|
|
95
|
+
// Create and return Undici ProxyAgent compatible with fetch's dispatcher option
|
|
96
|
+
return new ProxyAgent(normalizedProxyUrl);
|
|
97
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getCurrentLogLevel } from "./logging.js";
|
|
2
|
+
import { packageVersion } from "./index.js";
|
|
3
|
+
export function createConfigResource() {
|
|
4
|
+
const config = {
|
|
5
|
+
serverInfo: {
|
|
6
|
+
name: "ihor-sokoliuk/mcp-searxng",
|
|
7
|
+
version: packageVersion,
|
|
8
|
+
description: "MCP server for SearXNG integration via Gateway API"
|
|
9
|
+
},
|
|
10
|
+
environment: {
|
|
11
|
+
gatewayUrl: process.env.GATEWAY_URL || "http://115.190.91.253:80 (default)",
|
|
12
|
+
hasAuth: !!(process.env.AUTH_USERNAME && process.env.AUTH_PASSWORD),
|
|
13
|
+
hasProxy: !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy),
|
|
14
|
+
hasNoProxy: !!(process.env.NO_PROXY || process.env.no_proxy),
|
|
15
|
+
nodeVersion: process.version,
|
|
16
|
+
currentLogLevel: getCurrentLogLevel()
|
|
17
|
+
},
|
|
18
|
+
capabilities: {
|
|
19
|
+
tools: ["searxng_web_search", "web_url_read"],
|
|
20
|
+
logging: true,
|
|
21
|
+
resources: true,
|
|
22
|
+
transports: process.env.MCP_HTTP_PORT ? ["stdio", "http"] : ["stdio"]
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return JSON.stringify(config, null, 2);
|
|
26
|
+
}
|
|
27
|
+
export function createHelpResource() {
|
|
28
|
+
return `# SearXNG MCP Server Help
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
This is a Model Context Protocol (MCP) server that provides web search capabilities through a Gateway API and URL content reading functionality.
|
|
32
|
+
|
|
33
|
+
## Available Tools
|
|
34
|
+
|
|
35
|
+
### 1. searxng_web_search
|
|
36
|
+
Performs web searches using the configured Gateway API.
|
|
37
|
+
|
|
38
|
+
**Parameters:**
|
|
39
|
+
- \`query\` (required): The search query string
|
|
40
|
+
- \`pageno\` (optional): Page number (default: 1)
|
|
41
|
+
- \`time_range\` (optional): Filter by time - "day", "month", or "year"
|
|
42
|
+
- \`language\` (optional): Language code like "en", "fr", "de" (default: "all")
|
|
43
|
+
- \`safesearch\` (optional): Safe search level - "0" (none), "1" (moderate), "2" (strict)
|
|
44
|
+
|
|
45
|
+
### 2. web_url_read
|
|
46
|
+
Reads and converts web page content to Markdown format via Gateway API.
|
|
47
|
+
|
|
48
|
+
**Parameters:**
|
|
49
|
+
- \`url\` (required): The URL to fetch and convert
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
### Optional Environment Variables
|
|
54
|
+
- \`GATEWAY_URL\`: URL of the Gateway API (default: http://115.190.91.253:80)
|
|
55
|
+
|
|
56
|
+
### Optional Environment Variables
|
|
57
|
+
- \`AUTH_USERNAME\` & \`AUTH_PASSWORD\`: Basic authentication for Gateway
|
|
58
|
+
- \`HTTP_PROXY\` / \`HTTPS_PROXY\`: Proxy server configuration
|
|
59
|
+
- \`NO_PROXY\` / \`no_proxy\`: Comma-separated list of hosts to bypass proxy
|
|
60
|
+
- \`MCP_HTTP_PORT\`: Enable HTTP transport on specified port
|
|
61
|
+
|
|
62
|
+
## Transport Modes
|
|
63
|
+
|
|
64
|
+
### STDIO (Default)
|
|
65
|
+
Standard input/output transport for desktop clients like Claude Desktop.
|
|
66
|
+
|
|
67
|
+
### HTTP (Optional)
|
|
68
|
+
RESTful HTTP transport for web applications. Set \`MCP_HTTP_PORT\` to enable.
|
|
69
|
+
|
|
70
|
+
## Usage Examples
|
|
71
|
+
|
|
72
|
+
### Search for recent news
|
|
73
|
+
\`\`\`
|
|
74
|
+
Tool: searxng_web_search
|
|
75
|
+
Args: {"query": "latest AI developments", "time_range": "day"}
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
### Read a specific article
|
|
79
|
+
\`\`\`
|
|
80
|
+
Tool: web_url_read
|
|
81
|
+
Args: {"url": "https://example.com/article"}
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
## Troubleshooting
|
|
85
|
+
|
|
86
|
+
1. **Network errors**: Check if Gateway API is running and accessible
|
|
87
|
+
2. **Empty results**: Try different search terms or check Gateway service
|
|
88
|
+
3. **Timeout errors**: The server has a 10-second timeout for URL fetching
|
|
89
|
+
|
|
90
|
+
Use logging level "debug" for detailed request information.
|
|
91
|
+
|
|
92
|
+
## Current Configuration
|
|
93
|
+
See the "Current Configuration" resource for live settings.
|
|
94
|
+
`;
|
|
95
|
+
}
|
package/dist/search.d.ts
ADDED
package/dist/search.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createProxyAgent } from "./proxy.js";
|
|
2
|
+
import { logMessage } from "./logging.js";
|
|
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) {
|
|
5
|
+
const startTime = Date.now();
|
|
6
|
+
// Build detailed log message with all parameters
|
|
7
|
+
const searchParams = [
|
|
8
|
+
`page ${pageno}`,
|
|
9
|
+
`lang: ${language}`,
|
|
10
|
+
time_range ? `time: ${time_range}` : null,
|
|
11
|
+
safesearch ? `safesearch: ${safesearch}` : null
|
|
12
|
+
].filter(Boolean).join(", ");
|
|
13
|
+
logMessage(server, "info", `Starting web search: "${query}" (${searchParams})`);
|
|
14
|
+
const gatewayUrl = process.env.GATEWAY_URL || "http://115.190.91.253:80";
|
|
15
|
+
// Validate that gatewayUrl is a valid URL
|
|
16
|
+
let parsedUrl;
|
|
17
|
+
try {
|
|
18
|
+
parsedUrl = new URL(gatewayUrl);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw createConfigurationError(`Invalid GATEWAY_URL format: ${gatewayUrl}. Use format: http://115.190.91.253:80`);
|
|
22
|
+
}
|
|
23
|
+
const url = new URL('/api/search/', parsedUrl);
|
|
24
|
+
url.searchParams.set("q", query);
|
|
25
|
+
url.searchParams.set("format", "json");
|
|
26
|
+
url.searchParams.set("pageno", pageno.toString());
|
|
27
|
+
if (time_range !== undefined &&
|
|
28
|
+
["day", "month", "year"].includes(time_range)) {
|
|
29
|
+
url.searchParams.set("time_range", time_range);
|
|
30
|
+
}
|
|
31
|
+
if (language && language !== "all") {
|
|
32
|
+
url.searchParams.set("language", language);
|
|
33
|
+
}
|
|
34
|
+
if (safesearch !== undefined && ["0", "1", "2"].includes(safesearch)) {
|
|
35
|
+
url.searchParams.set("safesearch", safesearch);
|
|
36
|
+
}
|
|
37
|
+
// Prepare request options with headers
|
|
38
|
+
const requestOptions = {
|
|
39
|
+
method: "GET"
|
|
40
|
+
};
|
|
41
|
+
// Add proxy dispatcher if proxy is configured
|
|
42
|
+
// Node.js fetch uses 'dispatcher' option for proxy, not 'agent'
|
|
43
|
+
const proxyAgent = createProxyAgent(url.toString());
|
|
44
|
+
if (proxyAgent) {
|
|
45
|
+
requestOptions.dispatcher = proxyAgent;
|
|
46
|
+
}
|
|
47
|
+
// Add basic authentication if credentials are provided
|
|
48
|
+
const username = process.env.AUTH_USERNAME;
|
|
49
|
+
const password = process.env.AUTH_PASSWORD;
|
|
50
|
+
if (username && password) {
|
|
51
|
+
const base64Auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
52
|
+
requestOptions.headers = {
|
|
53
|
+
...requestOptions.headers,
|
|
54
|
+
'Authorization': `Basic ${base64Auth}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Add User-Agent header if configured
|
|
58
|
+
const userAgent = process.env.USER_AGENT;
|
|
59
|
+
if (userAgent) {
|
|
60
|
+
requestOptions.headers = {
|
|
61
|
+
...requestOptions.headers,
|
|
62
|
+
'User-Agent': userAgent
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Fetch with enhanced error handling
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
logMessage(server, "info", `Making request to: ${url.toString()}`);
|
|
69
|
+
response = await fetch(url.toString(), requestOptions);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logMessage(server, "error", `Network error during search request: ${error.message}`, { query, url: url.toString() });
|
|
73
|
+
const context = {
|
|
74
|
+
url: url.toString(),
|
|
75
|
+
gatewayUrl,
|
|
76
|
+
proxyAgent: !!proxyAgent,
|
|
77
|
+
username
|
|
78
|
+
};
|
|
79
|
+
throw createNetworkError(error, context);
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
let responseBody;
|
|
83
|
+
try {
|
|
84
|
+
responseBody = await response.text();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
responseBody = '[Could not read response body]';
|
|
88
|
+
}
|
|
89
|
+
const context = {
|
|
90
|
+
url: url.toString(),
|
|
91
|
+
gatewayUrl
|
|
92
|
+
};
|
|
93
|
+
throw createServerError(response.status, response.statusText, responseBody, context);
|
|
94
|
+
}
|
|
95
|
+
// Parse JSON response
|
|
96
|
+
let data;
|
|
97
|
+
try {
|
|
98
|
+
data = (await response.json());
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
let responseText;
|
|
102
|
+
try {
|
|
103
|
+
responseText = await response.text();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
responseText = '[Could not read response text]';
|
|
107
|
+
}
|
|
108
|
+
const context = { url: url.toString() };
|
|
109
|
+
throw createJSONError(responseText, context);
|
|
110
|
+
}
|
|
111
|
+
if (!data.results) {
|
|
112
|
+
const context = { url: url.toString(), query };
|
|
113
|
+
throw createDataError(data, context);
|
|
114
|
+
}
|
|
115
|
+
const results = data.results.map((result) => ({
|
|
116
|
+
title: result.title || "",
|
|
117
|
+
content: result.content || "",
|
|
118
|
+
url: result.url || "",
|
|
119
|
+
score: result.score || 0,
|
|
120
|
+
}));
|
|
121
|
+
if (results.length === 0) {
|
|
122
|
+
logMessage(server, "info", `No results found for query: "${query}"`);
|
|
123
|
+
return createNoResultsMessage(query);
|
|
124
|
+
}
|
|
125
|
+
const duration = Date.now() - startTime;
|
|
126
|
+
logMessage(server, "info", `Search completed: "${query}" (${searchParams}) - ${results.length} results in ${duration}ms`);
|
|
127
|
+
return results
|
|
128
|
+
.map((r) => `Title: ${r.title}\nDescription: ${r.content}\nURL: ${r.url}\nRelevance Score: ${r.score.toFixed(3)}`)
|
|
129
|
+
.join("\n\n");
|
|
130
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export interface SearXNGWeb {
|
|
3
|
+
results: Array<{
|
|
4
|
+
title: string;
|
|
5
|
+
content: string;
|
|
6
|
+
url: string;
|
|
7
|
+
score: number;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export declare function isSearXNGWebSearchArgs(args: unknown): args is {
|
|
11
|
+
query: string;
|
|
12
|
+
pageno?: number;
|
|
13
|
+
time_range?: string;
|
|
14
|
+
language?: string;
|
|
15
|
+
safesearch?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const WEB_SEARCH_TOOL: Tool;
|
|
18
|
+
export declare const READ_URL_TOOL: Tool;
|