@amplify-studio/open-mcp 0.9.0 → 0.10.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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Zhipu AI API Client
3
+ * Handles HTTP requests to Zhipu AI vision and image generation APIs
4
+ */
5
+ export interface VisionContentItem {
6
+ type: 'image_url' | 'video_url' | 'file_url';
7
+ image_url?: {
8
+ url: string;
9
+ };
10
+ video_url?: {
11
+ url: string;
12
+ };
13
+ file_url?: {
14
+ url: string;
15
+ };
16
+ }
17
+ interface VisionMessage {
18
+ role: 'user' | 'assistant' | 'system';
19
+ content: Array<VisionContentItem | {
20
+ type: 'text';
21
+ text: string;
22
+ }>;
23
+ }
24
+ /**
25
+ * Call GLM-4.6V-Flash for image/video/document understanding
26
+ */
27
+ export declare function callVisionAPI(messages: VisionMessage[], thinking?: boolean): Promise<string>;
28
+ /**
29
+ * Call Cogview-3-Flash for image generation
30
+ */
31
+ export declare function callImageGenAPI(prompt: string, size?: string): Promise<string>;
32
+ export {};
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Zhipu AI API Client
3
+ * Handles HTTP requests to Zhipu AI vision and image generation APIs
4
+ */
5
+ const ZHIPU_API_BASE = 'https://open.bigmodel.cn/api/paas/v4';
6
+ function getZhipuApiKey() {
7
+ const apiKey = process.env.ZHIPUAI_API_KEY;
8
+ if (!apiKey) {
9
+ throw new Error('ZHIPUAI_API_KEY environment variable is required');
10
+ }
11
+ return apiKey;
12
+ }
13
+ async function handleAPIError(response) {
14
+ const errorData = await response.json().catch(() => ({}));
15
+ const errorMessage = errorData.error?.message || `HTTP ${response.status}`;
16
+ const errorPrefix = response.status === 401
17
+ ? 'Authentication failed'
18
+ : response.status >= 500
19
+ ? 'Server error'
20
+ : 'API error';
21
+ throw new Error(`${errorPrefix}: ${errorMessage}`);
22
+ }
23
+ async function fetchZhipuAPI(endpoint, body) {
24
+ const response = await fetch(`${ZHIPU_API_BASE}${endpoint}`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ 'Authorization': `Bearer ${getZhipuApiKey()}`
29
+ },
30
+ body: JSON.stringify(body)
31
+ });
32
+ if (!response.ok) {
33
+ await handleAPIError(response);
34
+ }
35
+ return response;
36
+ }
37
+ /**
38
+ * Call GLM-4.6V-Flash for image/video/document understanding
39
+ */
40
+ export async function callVisionAPI(messages, thinking = false) {
41
+ const response = await fetchZhipuAPI('/chat/completions', {
42
+ model: 'glm-4.6v-flash',
43
+ messages,
44
+ thinking: {
45
+ type: thinking ? 'enabled' : 'disabled'
46
+ }
47
+ });
48
+ const data = await response.json();
49
+ if (!data.choices?.[0]?.message?.content) {
50
+ throw new Error('No response from vision API');
51
+ }
52
+ return data.choices[0].message.content;
53
+ }
54
+ /**
55
+ * Call Cogview-3-Flash for image generation
56
+ */
57
+ export async function callImageGenAPI(prompt, size = '1024x1024') {
58
+ const response = await fetchZhipuAPI('/images/generations', {
59
+ model: 'cogview-3-flash',
60
+ prompt,
61
+ size
62
+ });
63
+ const data = await response.json();
64
+ if (!data.data?.[0]?.url) {
65
+ throw new Error('No images generated');
66
+ }
67
+ return data.data[0].url;
68
+ }
@@ -2,28 +2,22 @@
2
2
  * Concise error handling for MCP SearXNG server
3
3
  * Provides clear, focused error messages that identify the root cause
4
4
  */
5
+ export declare const GATEWAY_URL_REQUIRED_MESSAGE = "GATEWAY_URL is required. Set it to your Gateway API URL (e.g., http://your-gateway.com:80)";
5
6
  export interface ErrorContext {
6
7
  url?: string;
7
- searxngUrl?: string;
8
8
  gatewayUrl?: string;
9
- proxyAgent?: boolean;
10
- username?: string;
11
9
  timeout?: number;
12
- query?: string;
13
10
  }
14
11
  export declare class MCPSearXNGError extends Error {
15
12
  constructor(message: string);
16
13
  }
17
14
  export declare function createConfigurationError(message: string): MCPSearXNGError;
18
15
  export declare function createNetworkError(error: any, context: ErrorContext): MCPSearXNGError;
19
- export declare function createServerError(status: number, statusText: string, responseBody: string, context: ErrorContext): MCPSearXNGError;
20
- export declare function createJSONError(responseText: string, context: ErrorContext): MCPSearXNGError;
21
- export declare function createDataError(data: any, context: ErrorContext): MCPSearXNGError;
16
+ export declare function createServerError(status: number, statusText: string, _responseBody: string, context: ErrorContext): MCPSearXNGError;
22
17
  export declare function createNoResultsMessage(query: string): string;
23
18
  export declare function createURLFormatError(url: string): MCPSearXNGError;
24
19
  export declare function createContentError(message: string, url: string): MCPSearXNGError;
25
- export declare function createConversionError(error: any, url: string, htmlContent: string): MCPSearXNGError;
26
20
  export declare function createTimeoutError(timeout: number, url: string): MCPSearXNGError;
27
- export declare function createEmptyContentWarning(url: string, htmlLength: number, htmlPreview: string): string;
21
+ export declare function createEmptyContentWarning(url: string): string;
28
22
  export declare function createUnexpectedError(error: any, context: ErrorContext): MCPSearXNGError;
29
23
  export declare function validateEnvironment(): string | null;
@@ -2,6 +2,7 @@
2
2
  * Concise error handling for MCP SearXNG server
3
3
  * Provides clear, focused error messages that identify the root cause
4
4
  */
5
+ export const GATEWAY_URL_REQUIRED_MESSAGE = "GATEWAY_URL is required. Set it to your Gateway API URL (e.g., http://your-gateway.com:80)";
5
6
  export class MCPSearXNGError extends Error {
6
7
  constructor(message) {
7
8
  super(message);
@@ -12,7 +13,7 @@ export function createConfigurationError(message) {
12
13
  return new MCPSearXNGError(`🔧 Configuration Error: ${message}`);
13
14
  }
14
15
  export function createNetworkError(error, context) {
15
- const target = context.searxngUrl ? 'SearXNG server' : (context.gatewayUrl ? 'Gateway server' : 'website');
16
+ const target = context.gatewayUrl ? 'Gateway server' : 'target server';
16
17
  if (error.code === 'ECONNREFUSED') {
17
18
  return new MCPSearXNGError(`🌐 Connection Error: ${target} is not responding (${context.url})`);
18
19
  }
@@ -26,27 +27,22 @@ export function createNetworkError(error, context) {
26
27
  if (error.message?.includes('certificate')) {
27
28
  return new MCPSearXNGError(`🌐 SSL Error: Certificate problem with ${target}`);
28
29
  }
29
- // For generic fetch failures, provide root cause guidance
30
30
  const errorMsg = error.message || error.code || 'Connection failed';
31
31
  if (errorMsg === 'fetch failed' || errorMsg === 'Connection failed') {
32
- const guidance = context.searxngUrl
33
- ? 'Check if the SEARXNG_URL is correct and the SearXNG server is available'
34
- : (context.gatewayUrl
35
- ? 'Check if the GATEWAY_URL is correct and the Gateway server is available'
36
- : 'Check if the website URL is accessible');
32
+ const guidance = context.gatewayUrl
33
+ ? 'Check if the GATEWAY_URL is correct and the Gateway server is available'
34
+ : 'Check if the target URL is accessible';
37
35
  return new MCPSearXNGError(`🌐 Network Error: ${errorMsg}. ${guidance}`);
38
36
  }
39
37
  return new MCPSearXNGError(`🌐 Network Error: ${errorMsg}`);
40
38
  }
41
- export function createServerError(status, statusText, responseBody, context) {
42
- const target = context.searxngUrl ? 'SearXNG server' : (context.gatewayUrl ? 'Gateway server' : 'Website');
39
+ export function createServerError(status, statusText, _responseBody, context) {
40
+ const target = context.gatewayUrl ? 'Gateway server' : 'Website';
43
41
  if (status === 403) {
44
- const reason = context.searxngUrl ? 'Authentication required or IP blocked' : 'Access blocked (bot detection or geo-restriction)';
45
- return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${reason}`);
42
+ return new MCPSearXNGError(`🚫 ${target} Error (${status}): Access blocked (bot detection or geo-restriction)`);
46
43
  }
47
44
  if (status === 404) {
48
- const reason = context.searxngUrl ? 'Search endpoint not found' : 'Page not found';
49
- return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${reason}`);
45
+ return new MCPSearXNGError(`🚫 ${target} Error (${status}): Page not found`);
50
46
  }
51
47
  if (status === 429) {
52
48
  return new MCPSearXNGError(`🚫 ${target} Error (${status}): Rate limit exceeded`);
@@ -56,15 +52,8 @@ export function createServerError(status, statusText, responseBody, context) {
56
52
  }
57
53
  return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${statusText}`);
58
54
  }
59
- export function createJSONError(responseText, context) {
60
- const preview = responseText.substring(0, 100).replace(/\n/g, ' ');
61
- return new MCPSearXNGError(`🔍 SearXNG Response Error: Invalid JSON format. Response: "${preview}..."`);
62
- }
63
- export function createDataError(data, context) {
64
- return new MCPSearXNGError(`🔍 SearXNG Data Error: Missing results array in response`);
65
- }
66
55
  export function createNoResultsMessage(query) {
67
- return `🔍 No results found for "${query}". Try different search terms or check if SearXNG search engines are working.`;
56
+ return `🔍 No results found for "${query}". Try different search terms or check if the Gateway service is working.`;
68
57
  }
69
58
  export function createURLFormatError(url) {
70
59
  return new MCPSearXNGError(`🔧 URL Format Error: Invalid URL "${url}"`);
@@ -72,14 +61,11 @@ export function createURLFormatError(url) {
72
61
  export function createContentError(message, url) {
73
62
  return new MCPSearXNGError(`📄 Content Error: ${message} (${url})`);
74
63
  }
75
- export function createConversionError(error, url, htmlContent) {
76
- return new MCPSearXNGError(`🔄 Conversion Error: Cannot convert HTML to Markdown (${url})`);
77
- }
78
64
  export function createTimeoutError(timeout, url) {
79
65
  const hostname = new URL(url).hostname;
80
66
  return new MCPSearXNGError(`⏱️ Timeout Error: ${hostname} took longer than ${timeout}ms to respond`);
81
67
  }
82
- export function createEmptyContentWarning(url, htmlLength, htmlPreview) {
68
+ export function createEmptyContentWarning(url) {
83
69
  return `📄 Content Warning: Page fetched but appears empty after conversion (${url}). May contain only media or require JavaScript.`;
84
70
  }
85
71
  export function createUnexpectedError(error, context) {
@@ -87,28 +73,30 @@ export function createUnexpectedError(error, context) {
87
73
  }
88
74
  export function validateEnvironment() {
89
75
  const issues = [];
76
+ // Validate GATEWAY_URL if provided
90
77
  const gatewayUrl = process.env.GATEWAY_URL;
91
78
  if (gatewayUrl) {
92
79
  try {
93
80
  const url = new URL(gatewayUrl);
94
81
  if (!['http:', 'https:'].includes(url.protocol)) {
95
- issues.push(`GATEWAY_URL invalid protocol: ${url.protocol}`);
82
+ issues.push(`GATEWAY_URL has invalid protocol: ${url.protocol}`);
96
83
  }
97
84
  }
98
- catch (error) {
99
- issues.push(`GATEWAY_URL invalid format: ${gatewayUrl}`);
85
+ catch {
86
+ issues.push(`GATEWAY_URL has invalid format: ${gatewayUrl}`);
100
87
  }
101
88
  }
102
- const authUsername = process.env.AUTH_USERNAME;
103
- const authPassword = process.env.AUTH_PASSWORD;
104
- if (authUsername && !authPassword) {
105
- issues.push("AUTH_USERNAME set but AUTH_PASSWORD missing");
89
+ // Validate auth credentials are paired correctly
90
+ const hasUsername = process.env.AUTH_USERNAME;
91
+ const hasPassword = process.env.AUTH_PASSWORD;
92
+ if (hasUsername && !hasPassword) {
93
+ issues.push("AUTH_USERNAME is set but AUTH_PASSWORD is missing");
106
94
  }
107
- else if (!authUsername && authPassword) {
108
- issues.push("AUTH_PASSWORD set but AUTH_USERNAME missing");
95
+ if (hasPassword && !hasUsername) {
96
+ issues.push("AUTH_PASSWORD is set but AUTH_USERNAME is missing");
109
97
  }
110
98
  if (issues.length === 0) {
111
99
  return null;
112
100
  }
113
- return `⚠️ Configuration Issues: ${issues.join(', ')}. GATEWAY_URL is optional (defaults to http://115.190.91.253:80)`;
101
+ return `⚠️ Configuration Issues: ${issues.join(', ')}. GATEWAY_URL must be set to a valid HTTP(S) URL`;
114
102
  }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,3 @@
1
1
  #!/usr/bin/env node
2
- declare const packageVersion = "0.8.0";
2
+ declare const packageVersion = "0.9.0";
3
3
  export { packageVersion };
4
- export declare function isWebUrlReadArgs(args: unknown): args is {
5
- url: string;
6
- startChar?: number;
7
- maxLength?: number;
8
- section?: string;
9
- paragraphRange?: string;
10
- readHeadings?: boolean;
11
- };
package/dist/index.js CHANGED
@@ -3,51 +3,19 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  // Import modularized functionality
6
- import { WEB_SEARCH_TOOL, READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
6
+ import { IMAGE_GENERATE_TOOL, IMAGE_UNDERSTAND_TOOL, READ_URL_TOOL, WEB_SEARCH_TOOL, isImageGenerateArgs, isImageUnderstandArgs, isSearXNGWebSearchArgs, isWebUrlReadArgs, } from "./types.js";
7
7
  import { logMessage, setLogLevel } from "./logging.js";
8
8
  import { performWebSearch } from "./search.js";
9
9
  import { fetchAndConvertToMarkdown } from "./url-reader.js";
10
+ import { understandImage } from "./tools/image-understand.js";
11
+ import { generateImage } from "./tools/image-generate.js";
10
12
  import { createConfigResource, createHelpResource } from "./resources.js";
11
13
  import { createHttpServer } from "./http-server.js";
12
14
  import { validateEnvironment as validateEnv } from "./error-handler.js";
13
15
  // Use a static version string that will be updated by the version script
14
- const packageVersion = "0.8.0";
16
+ const packageVersion = "0.9.0";
15
17
  // Export the version for use in other modules
16
18
  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
19
  // Server implementation
52
20
  const server = new Server({
53
21
  name: "ihor-sokoliuk/mcp-searxng",
@@ -65,59 +33,79 @@ const server = new Server({
65
33
  description: READ_URL_TOOL.description,
66
34
  schema: READ_URL_TOOL.inputSchema,
67
35
  },
36
+ image_understand: {
37
+ description: IMAGE_UNDERSTAND_TOOL.description,
38
+ schema: IMAGE_UNDERSTAND_TOOL.inputSchema,
39
+ },
40
+ image_generate: {
41
+ description: IMAGE_GENERATE_TOOL.description,
42
+ schema: IMAGE_GENERATE_TOOL.inputSchema,
43
+ },
68
44
  },
69
45
  },
70
46
  });
71
47
  // List tools handler
72
48
  server.setRequestHandler(ListToolsRequestSchema, async () => {
49
+ resetActivityTimeout();
73
50
  logMessage(server, "debug", "Handling list_tools request");
74
51
  return {
75
- tools: [WEB_SEARCH_TOOL, READ_URL_TOOL],
52
+ tools: [
53
+ WEB_SEARCH_TOOL,
54
+ READ_URL_TOOL,
55
+ IMAGE_UNDERSTAND_TOOL,
56
+ IMAGE_GENERATE_TOOL
57
+ ],
76
58
  };
77
59
  });
78
60
  // Call tool handler
79
61
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
62
+ resetActivityTimeout();
80
63
  const { name, arguments: args } = request.params;
81
64
  logMessage(server, "debug", `Handling call_tool request: ${name}`);
82
65
  try {
83
- if (name === "searxng_web_search") {
84
- if (!isSearXNGWebSearchArgs(args)) {
85
- throw new Error("Invalid arguments for web search");
66
+ let result;
67
+ switch (name) {
68
+ case "searxng_web_search": {
69
+ if (!isSearXNGWebSearchArgs(args)) {
70
+ throw new Error("Invalid arguments for web search");
71
+ }
72
+ result = await performWebSearch(server, args.query, args.limit);
73
+ break;
86
74
  }
87
- const result = await performWebSearch(server, args.query, args.limit);
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");
75
+ case "web_url_read": {
76
+ if (!isWebUrlReadArgs(args)) {
77
+ throw new Error("Invalid arguments for URL reading");
78
+ }
79
+ // Use default 30s timeout (undefined) instead of hardcoded 10s
80
+ result = await fetchAndConvertToMarkdown(server, args.url, undefined, {
81
+ startChar: args.startChar,
82
+ maxLength: args.maxLength,
83
+ section: args.section,
84
+ paragraphRange: args.paragraphRange,
85
+ readHeadings: args.readHeadings,
86
+ });
87
+ break;
100
88
  }
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}`);
89
+ case "image_understand": {
90
+ if (!isImageUnderstandArgs(args)) {
91
+ throw new Error("Invalid arguments for image understanding");
92
+ }
93
+ result = await understandImage(args);
94
+ break;
95
+ }
96
+ case "image_generate": {
97
+ if (!isImageGenerateArgs(args)) {
98
+ throw new Error("Invalid arguments for image generation");
99
+ }
100
+ result = await generateImage(args);
101
+ break;
102
+ }
103
+ default:
104
+ throw new Error(`Unknown tool: ${name}`);
120
105
  }
106
+ return {
107
+ content: [{ type: "text", text: result }],
108
+ };
121
109
  }
122
110
  catch (error) {
123
111
  logMessage(server, "error", `Tool execution error: ${error instanceof Error ? error.message : String(error)}`, {
@@ -130,14 +118,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
130
118
  });
131
119
  // Logging level handler
132
120
  server.setRequestHandler(SetLevelRequestSchema, async (request) => {
121
+ resetActivityTimeout();
133
122
  const { level } = request.params;
134
123
  logMessage(server, "info", `Setting log level to: ${level}`);
135
- currentLogLevel = level;
136
124
  setLogLevel(level);
137
125
  return {};
138
126
  });
139
127
  // List resources handler
140
128
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
129
+ resetActivityTimeout();
141
130
  logMessage(server, "debug", "Handling list_resources request");
142
131
  return {
143
132
  resources: [
@@ -158,6 +147,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
158
147
  });
159
148
  // Read resource handler
160
149
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
150
+ resetActivityTimeout();
161
151
  const { uri } = request.params;
162
152
  logMessage(server, "debug", `Handling read_resource request for: ${uri}`);
163
153
  switch (uri) {
@@ -185,6 +175,45 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
185
175
  throw new Error(`Unknown resource: ${uri}`);
186
176
  }
187
177
  });
178
+ // Inactivity timeout: shut down after 3 minutes of no client requests
179
+ const INACTIVITY_TIMEOUT_MS = 180000; // 3 minutes
180
+ let activityTimeout;
181
+ /**
182
+ * Reset the inactivity timer. Called on every MCP request.
183
+ * If no request occurs within the timeout period, the server exits.
184
+ */
185
+ function resetActivityTimeout() {
186
+ clearTimeout(activityTimeout);
187
+ activityTimeout = setTimeout(() => {
188
+ logMessage(server, "info", `No activity for ${INACTIVITY_TIMEOUT_MS / 1000}s, shutting down`);
189
+ process.exit(0);
190
+ }, INACTIVITY_TIMEOUT_MS);
191
+ }
192
+ // Shutdown configuration
193
+ const SHUTDOWN_TIMEOUT_MS = 10000;
194
+ function setupShutdownHandlers(mode, httpServer) {
195
+ const shutdown = (signal) => {
196
+ clearTimeout(activityTimeout);
197
+ const logFn = mode === 'http' ? console.log : (msg) => logMessage(server, 'info', msg);
198
+ logFn(`Received ${signal}. Shutting down ${mode.toUpperCase()} server...`);
199
+ if (mode === 'http' && httpServer) {
200
+ const timeoutId = setTimeout(() => {
201
+ console.error('Forced shutdown after timeout');
202
+ process.exit(1);
203
+ }, SHUTDOWN_TIMEOUT_MS);
204
+ httpServer.close(() => {
205
+ clearTimeout(timeoutId);
206
+ console.log('HTTP server closed');
207
+ process.exit(0);
208
+ });
209
+ }
210
+ else {
211
+ process.exit(0);
212
+ }
213
+ };
214
+ process.on('SIGINT', () => shutdown('SIGINT'));
215
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
216
+ }
188
217
  // Main function
189
218
  async function main() {
190
219
  // Environment validation
@@ -193,14 +222,20 @@ async function main() {
193
222
  console.error(`❌ ${validationError}`);
194
223
  process.exit(1);
195
224
  }
225
+ // Validate Zhipu AI (warning only, don't block startup)
226
+ if (!process.env.ZHIPUAI_API_KEY) {
227
+ console.warn('WARNING: ZHIPUAI_API_KEY not set. Image tools will not work.');
228
+ }
196
229
  // Check for HTTP transport mode
197
230
  const httpPort = process.env.MCP_HTTP_PORT;
231
+ const gatewayUrlDisplay = process.env.GATEWAY_URL || "Not configured (required)";
198
232
  if (httpPort) {
199
233
  const port = parseInt(httpPort, 10);
200
234
  if (isNaN(port) || port < 1 || port > 65535) {
201
235
  console.error(`Invalid HTTP port: ${httpPort}. Must be between 1-65535.`);
202
236
  process.exit(1);
203
237
  }
238
+ console.log(`GATEWAY_URL: ${gatewayUrlDisplay}`);
204
239
  console.log(`Starting HTTP transport on port ${port}`);
205
240
  const app = await createHttpServer(server);
206
241
  const httpServer = app.listen(port, () => {
@@ -208,16 +243,7 @@ async function main() {
208
243
  console.log(`Health check: http://localhost:${port}/health`);
209
244
  console.log(`MCP endpoint: http://localhost:${port}/mcp`);
210
245
  });
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'));
246
+ setupShutdownHandlers('http', httpServer);
221
247
  }
222
248
  else {
223
249
  // Default STDIO transport
@@ -225,16 +251,27 @@ async function main() {
225
251
  if (process.stdin.isTTY) {
226
252
  console.log(`🔍 MCP SearXNG Server v${packageVersion} - Ready`);
227
253
  console.log("✅ Configuration valid");
228
- console.log(`🌐 Gateway URL: ${process.env.GATEWAY_URL || "http://115.190.91.253:80 (default)"}`);
254
+ console.log(`🌐 Gateway URL: ${gatewayUrlDisplay}`);
229
255
  console.log("📡 Waiting for MCP client connection via STDIO...\n");
230
256
  }
231
257
  const transport = new StdioServerTransport();
258
+ // Handle stdin close (when client disconnects)
259
+ const handleStdioClose = () => {
260
+ clearTimeout(activityTimeout);
261
+ logMessage(server, "info", "STDIO connection closed by client");
262
+ transport.close().then(() => process.exit(0));
263
+ };
264
+ // Listen for both stdin close and transport close
265
+ process.stdin.on('close', handleStdioClose);
266
+ transport.onclose = handleStdioClose;
232
267
  await server.connect(transport);
268
+ // Start inactivity timer after connection
269
+ resetActivityTimeout();
233
270
  // Log after connection is established
234
271
  logMessage(server, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
235
- logMessage(server, "info", `Log level: ${currentLogLevel}`);
236
272
  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)'}`);
273
+ logMessage(server, "info", `Gateway URL: ${gatewayUrlDisplay}`);
274
+ setupShutdownHandlers('stdio');
238
275
  }
239
276
  }
240
277
  // Handle uncaught errors