@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/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;
@@ -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
+ }
@@ -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,2 @@
1
+ export declare function createConfigResource(): string;
2
+ export declare function createHelpResource(): string;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ export declare function performWebSearch(server: Server, query: string, pageno?: number, time_range?: string, language?: string, safesearch?: string): Promise<string>;
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
+ }
@@ -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;