@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.
- package/README.md +352 -325
- package/dist/api/zhipu.d.ts +32 -0
- package/dist/api/zhipu.js +68 -0
- package/dist/error-handler.d.ts +3 -9
- package/dist/error-handler.js +23 -35
- package/dist/index.d.ts +1 -9
- package/dist/index.js +123 -86
- package/dist/resources.js +43 -16
- package/dist/search.js +25 -30
- package/dist/tools/image-generate.d.ts +5 -0
- package/dist/tools/image-generate.js +29 -0
- package/dist/tools/image-ocr.d.ts +5 -0
- package/dist/tools/image-ocr.js +102 -0
- package/dist/tools/image-understand.d.ts +5 -0
- package/dist/tools/image-understand.js +54 -0
- package/dist/types.d.ts +22 -7
- package/dist/types.js +112 -5
- package/dist/url-reader.d.ts +2 -1
- package/dist/url-reader.js +21 -27
- package/dist/utils/file-helper.d.ts +19 -0
- package/dist/utils/file-helper.js +77 -0
- package/package.json +4 -3
|
@@ -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
|
+
}
|
package/dist/error-handler.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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;
|
package/dist/error-handler.js
CHANGED
|
@@ -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.
|
|
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.
|
|
33
|
-
? 'Check if the
|
|
34
|
-
:
|
|
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,
|
|
42
|
-
const target = context.
|
|
39
|
+
export function createServerError(status, statusText, _responseBody, context) {
|
|
40
|
+
const target = context.gatewayUrl ? 'Gateway server' : 'Website';
|
|
43
41
|
if (status === 403) {
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
99
|
-
issues.push(`GATEWAY_URL invalid format: ${gatewayUrl}`);
|
|
85
|
+
catch {
|
|
86
|
+
issues.push(`GATEWAY_URL has invalid format: ${gatewayUrl}`);
|
|
100
87
|
}
|
|
101
88
|
}
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 {
|
|
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.
|
|
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: [
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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: ${
|
|
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: ${
|
|
273
|
+
logMessage(server, "info", `Gateway URL: ${gatewayUrlDisplay}`);
|
|
274
|
+
setupShutdownHandlers('stdio');
|
|
238
275
|
}
|
|
239
276
|
}
|
|
240
277
|
// Handle uncaught errors
|