@amplify-studio/open-mcp 0.8.2 → 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/dist/types.js CHANGED
@@ -1,40 +1,34 @@
1
1
  export function isSearXNGWebSearchArgs(args) {
2
- return (typeof args === "object" &&
3
- args !== null &&
4
- "query" in args &&
5
- typeof args.query === "string");
2
+ if (typeof args !== "object" ||
3
+ args === null ||
4
+ !("query" in args) ||
5
+ typeof args.query !== "string") {
6
+ return false;
7
+ }
8
+ const searchArgs = args;
9
+ if (searchArgs.limit !== undefined && (typeof searchArgs.limit !== "number" || searchArgs.limit < 1 || searchArgs.limit > 100)) {
10
+ return false;
11
+ }
12
+ return true;
6
13
  }
7
14
  export const WEB_SEARCH_TOOL = {
8
15
  name: "searxng_web_search",
9
- description: "Performs a web search using the SearXNG API, ideal for general queries, news, articles, and online content. " +
10
- "Use this for broad information gathering, recent events, or when you need diverse web sources.",
16
+ description: "Performs web search using the Gateway API Firecrawl search. " +
17
+ "Returns search results with title, content, and URL. " +
18
+ "Use this for general queries, news, articles, and online content.",
11
19
  inputSchema: {
12
20
  type: "object",
13
21
  properties: {
14
22
  query: {
15
23
  type: "string",
16
- description: "The search query. This is the main input for the web search",
24
+ description: "The search query string",
17
25
  },
18
- pageno: {
26
+ limit: {
19
27
  type: "number",
20
- description: "Search page number (starts at 1)",
21
- default: 1,
22
- },
23
- time_range: {
24
- type: "string",
25
- description: "Time range of search (day, month, year)",
26
- enum: ["day", "month", "year"],
27
- },
28
- language: {
29
- type: "string",
30
- description: "Language code for search results (e.g., 'en', 'fr', 'de'). Default is instance-dependent.",
31
- default: "all",
32
- },
33
- safesearch: {
34
- type: "string",
35
- description: "Safe search filter level (0: None, 1: Moderate, 2: Strict)",
36
- enum: ["0", "1", "2"],
37
- default: "0",
28
+ description: "Maximum number of results to return (default: 10, max: 100)",
29
+ default: 10,
30
+ minimum: 1,
31
+ maximum: 100,
38
32
  },
39
33
  },
40
34
  required: ["query"],
@@ -77,3 +71,103 @@ export const READ_URL_TOOL = {
77
71
  required: ["url"],
78
72
  },
79
73
  };
74
+ export const IMAGE_UNDERSTAND_TOOL = {
75
+ name: "image_understand",
76
+ description: "Understand and analyze images, videos, and documents using Zhipu GLM-4.6V-Flash model. " +
77
+ "Supports visual Q&A, content description, OCR, document parsing, video understanding, " +
78
+ "and frontend code replication from screenshots. " +
79
+ "Accepts file paths, URLs, or base64 data. " +
80
+ "Use this when you need to extract information from visual content or answer questions about images/videos.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ file: {
85
+ type: "string",
86
+ description: "File path, URL, or base64 data (image, video, or PDF)",
87
+ },
88
+ prompt: {
89
+ type: "string",
90
+ description: "Question or instruction for the visual content analysis",
91
+ },
92
+ thinking: {
93
+ type: "boolean",
94
+ description: "Enable deep thinking mode for complex reasoning (default: true)",
95
+ default: true,
96
+ },
97
+ },
98
+ required: ["file", "prompt"],
99
+ },
100
+ };
101
+ export const IMAGE_GENERATE_TOOL = {
102
+ name: "image_generate",
103
+ description: "Generate images from text descriptions using Zhipu Cogview-3-Flash model. " +
104
+ "Supports multiple resolutions. " +
105
+ "Use this when you need to create visual content from text prompts.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ prompt: {
110
+ type: "string",
111
+ description: "Text description of the image to generate",
112
+ },
113
+ size: {
114
+ type: "string",
115
+ enum: ["1024x1024", "768x1344", "864x1152", "1344x768", "1152x864", "1440x720", "720x1440"],
116
+ description: "Image size (default: 1024x1024)",
117
+ default: "1024x1024",
118
+ },
119
+ },
120
+ required: ["prompt"],
121
+ },
122
+ };
123
+ /**
124
+ * Generic type guard for checking if an object has a property of a specific type
125
+ */
126
+ function hasProperty(args, prop, type) {
127
+ return (typeof args === "object" &&
128
+ args !== null &&
129
+ prop in args &&
130
+ typeof args[prop] === type);
131
+ }
132
+ export function isImageUnderstandArgs(args) {
133
+ if (!hasProperty(args, 'prompt', 'string') || !hasProperty(args, 'file', 'string')) {
134
+ return false;
135
+ }
136
+ const typedArgs = args;
137
+ if (typedArgs.thinking !== undefined && typeof typedArgs.thinking !== 'boolean') {
138
+ return false;
139
+ }
140
+ return true;
141
+ }
142
+ export function isImageGenerateArgs(args) {
143
+ if (!hasProperty(args, 'prompt', 'string')) {
144
+ return false;
145
+ }
146
+ const typedArgs = args;
147
+ if (typedArgs.size !== undefined && typeof typedArgs.size !== 'string') {
148
+ return false;
149
+ }
150
+ return true;
151
+ }
152
+ export function isWebUrlReadArgs(args) {
153
+ if (!hasProperty(args, 'url', 'string')) {
154
+ return false;
155
+ }
156
+ const urlArgs = args;
157
+ if (urlArgs.startChar !== undefined && (typeof urlArgs.startChar !== 'number' || urlArgs.startChar < 0)) {
158
+ return false;
159
+ }
160
+ if (urlArgs.maxLength !== undefined && (typeof urlArgs.maxLength !== 'number' || urlArgs.maxLength < 1)) {
161
+ return false;
162
+ }
163
+ if (urlArgs.section !== undefined && typeof urlArgs.section !== 'string') {
164
+ return false;
165
+ }
166
+ if (urlArgs.paragraphRange !== undefined && typeof urlArgs.paragraphRange !== 'string') {
167
+ return false;
168
+ }
169
+ if (urlArgs.readHeadings !== undefined && typeof urlArgs.readHeadings !== 'boolean') {
170
+ return false;
171
+ }
172
+ return true;
173
+ }
@@ -6,5 +6,6 @@ interface PaginationOptions {
6
6
  paragraphRange?: string;
7
7
  readHeadings?: boolean;
8
8
  }
9
- export declare function fetchAndConvertToMarkdown(server: Server, url: string, timeoutMs?: number, paginationOptions?: PaginationOptions): Promise<string>;
9
+ export declare function fetchAndConvertToMarkdown(server: Server, url: string, timeoutMs?: number, // Increased default from 10s to 30s
10
+ paginationOptions?: PaginationOptions): Promise<string>;
10
11
  export {};
@@ -1,7 +1,6 @@
1
- import { createProxyAgent } from "./proxy.js";
2
1
  import { logMessage } from "./logging.js";
3
2
  import { urlCache } from "./cache.js";
4
- import { createURLFormatError, createNetworkError, createServerError, createContentError, createTimeoutError, createEmptyContentWarning, createUnexpectedError } from "./error-handler.js";
3
+ import { createURLFormatError, createConfigurationError, createNetworkError, createServerError, createContentError, createTimeoutError, createEmptyContentWarning, createUnexpectedError, GATEWAY_URL_REQUIRED_MESSAGE } from "./error-handler.js";
5
4
  function applyCharacterPagination(content, startChar = 0, maxLength) {
6
5
  if (startChar >= content.length) {
7
6
  return "";
@@ -99,7 +98,8 @@ function applyPaginationOptions(markdownContent, options) {
99
98
  }
100
99
  return result;
101
100
  }
102
- export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000, paginationOptions = {}) {
101
+ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 30000, // Increased default from 10s to 30s
102
+ paginationOptions = {}) {
103
103
  const startTime = Date.now();
104
104
  logMessage(server, "info", `Fetching URL: ${url}`);
105
105
  // Check cache first
@@ -112,44 +112,40 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
112
112
  return result;
113
113
  }
114
114
  // Validate URL format
115
- let parsedUrl;
116
115
  try {
117
- parsedUrl = new URL(url);
116
+ new URL(url);
118
117
  }
119
- catch (error) {
118
+ catch {
120
119
  logMessage(server, "error", `Invalid URL format: ${url}`);
121
120
  throw createURLFormatError(url);
122
121
  }
123
122
  // Build gateway API URL
124
- const gatewayUrl = process.env.GATEWAY_URL || "http://115.190.91.253:80";
123
+ const gatewayUrl = process.env.GATEWAY_URL;
124
+ if (!gatewayUrl) {
125
+ throw createConfigurationError(GATEWAY_URL_REQUIRED_MESSAGE);
126
+ }
125
127
  const gatewayApiUrl = `${gatewayUrl}/api/read/${encodeURIComponent(url)}`;
126
128
  // Create an AbortController instance
127
129
  const controller = new AbortController();
128
130
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
129
131
  try {
130
- // Prepare request options with proxy support
132
+ // Prepare request options
131
133
  const requestOptions = {
132
134
  signal: controller.signal,
133
135
  };
134
- // Add proxy dispatcher if proxy is configured
135
- // Node.js fetch uses 'dispatcher' option for proxy, not 'agent'
136
- const proxyAgent = createProxyAgent(gatewayApiUrl);
137
- if (proxyAgent) {
138
- requestOptions.dispatcher = proxyAgent;
139
- }
140
136
  let response;
141
137
  try {
142
- // Fetch the URL via gateway API with the abort signal
138
+ logMessage(server, "info", `Fetching from Gateway: ${gatewayApiUrl}`);
143
139
  response = await fetch(gatewayApiUrl, requestOptions);
140
+ logMessage(server, "info", `Gateway response status: ${response.status}`);
144
141
  }
145
142
  catch (error) {
146
- const context = {
147
- url,
148
- gatewayUrl,
149
- proxyAgent: !!proxyAgent,
150
- timeout: timeoutMs
151
- };
152
- throw createNetworkError(error, context);
143
+ if (error.name === 'AbortError') {
144
+ logMessage(server, "error", `Gateway request timeout: ${url} (${timeoutMs}ms)`);
145
+ throw createTimeoutError(timeoutMs, url);
146
+ }
147
+ logMessage(server, "error", `Gateway network error: ${error.message}`, { url, gatewayUrl });
148
+ throw createNetworkError(error, { url, gatewayUrl, timeout: timeoutMs });
153
149
  }
154
150
  if (!response.ok) {
155
151
  let responseBody;
@@ -159,8 +155,7 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
159
155
  catch {
160
156
  responseBody = '[Could not read response body]';
161
157
  }
162
- const context = { url, gatewayUrl };
163
- throw createServerError(response.status, response.statusText, responseBody, context);
158
+ throw createServerError(response.status, response.statusText, responseBody, { url, gatewayUrl });
164
159
  }
165
160
  // Retrieve content from gateway API (JSON response)
166
161
  let markdownContent;
@@ -180,7 +175,7 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
180
175
  }
181
176
  if (!markdownContent || markdownContent.trim().length === 0) {
182
177
  logMessage(server, "warning", `Empty content from gateway: ${url}`);
183
- return createEmptyContentWarning(url, 0, "");
178
+ return createEmptyContentWarning(url);
184
179
  }
185
180
  // Cache the markdown content from gateway
186
181
  urlCache.set(url, "", markdownContent);
@@ -209,8 +204,7 @@ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000,
209
204
  }
210
205
  // Catch any unexpected errors
211
206
  logMessage(server, "error", `Unexpected error fetching URL: ${url}`, error);
212
- const context = { url };
213
- throw createUnexpectedError(error, context);
207
+ throw createUnexpectedError(error, { url });
214
208
  }
215
209
  finally {
216
210
  // Clean up the timeout to prevent memory leaks
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Detect content type based on file extension
3
+ */
4
+ export declare function detectFileType(filePath: string): 'image' | 'video' | 'file' | null;
5
+ /**
6
+ * Normalize user input - detect if it's a local path, URL, or base64
7
+ */
8
+ export declare function normalizeInput(input: string): {
9
+ type: 'local' | 'url' | 'base64';
10
+ value: string;
11
+ };
12
+ /**
13
+ * Read local file and convert to base64
14
+ */
15
+ export declare function readAsBase64(filePath: string): Promise<string>;
16
+ /**
17
+ * Get MIME type based on file extension
18
+ */
19
+ export declare function getMimeType(filePath: string): string;
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ // MIME type mapping for common extensions
4
+ const MIME_TYPES = {
5
+ // Images
6
+ '.png': 'image/png',
7
+ '.jpg': 'image/jpeg',
8
+ '.jpeg': 'image/jpeg',
9
+ '.gif': 'image/gif',
10
+ '.webp': 'image/webp',
11
+ '.bmp': 'image/bmp',
12
+ // Videos
13
+ '.mp4': 'video/mp4',
14
+ '.mov': 'video/quicktime',
15
+ '.avi': 'video/x-msvideo',
16
+ '.mkv': 'video/x-matroska',
17
+ '.webm': 'video/webm',
18
+ // Documents
19
+ '.pdf': 'application/pdf',
20
+ '.txt': 'text/plain',
21
+ '.doc': 'application/msword',
22
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
23
+ '.xls': 'application/vnd.ms-excel',
24
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
25
+ };
26
+ // Content type mappings by extension
27
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
28
+ const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm']);
29
+ const FILE_EXTENSIONS = new Set(['.pdf', '.txt', '.doc', '.docx', '.xls', '.xlsx']);
30
+ /**
31
+ * Detect content type based on file extension
32
+ */
33
+ export function detectFileType(filePath) {
34
+ const ext = path.extname(filePath).toLowerCase();
35
+ if (IMAGE_EXTENSIONS.has(ext))
36
+ return 'image';
37
+ if (VIDEO_EXTENSIONS.has(ext))
38
+ return 'video';
39
+ if (FILE_EXTENSIONS.has(ext))
40
+ return 'file';
41
+ return null;
42
+ }
43
+ /**
44
+ * Normalize user input - detect if it's a local path, URL, or base64
45
+ */
46
+ export function normalizeInput(input) {
47
+ if (input.startsWith('data:')) {
48
+ return { type: 'base64', value: input };
49
+ }
50
+ if (input.startsWith('http://') || input.startsWith('https://')) {
51
+ return { type: 'url', value: input };
52
+ }
53
+ if (/^[A-Za-z0-9+/=]{20,}$/.test(input) && !input.includes(' ')) {
54
+ return { type: 'base64', value: input };
55
+ }
56
+ return { type: 'local', value: input };
57
+ }
58
+ /**
59
+ * Read local file and convert to base64
60
+ */
61
+ export async function readAsBase64(filePath) {
62
+ try {
63
+ const buffer = await fs.readFile(filePath);
64
+ return buffer.toString('base64');
65
+ }
66
+ catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ throw new Error(`Failed to read file: ${filePath}. Error: ${message}`);
69
+ }
70
+ }
71
+ /**
72
+ * Get MIME type based on file extension
73
+ */
74
+ export function getMimeType(filePath) {
75
+ const ext = path.extname(filePath).toLowerCase();
76
+ return MIME_TYPES[ext] || 'application/octet-stream';
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amplify-studio/open-mcp",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "Open MCP server for web search and URL reading via Gateway API",
5
5
  "license": "MIT",
6
6
  "author": "Amplify Studio (https://github.com/amplify-studio)",
@@ -39,7 +39,7 @@
39
39
  "test": "SEARXNG_URL=https://test-searx.example.com tsx __tests__/run-all.ts",
40
40
  "test:coverage": "SEARXNG_URL=https://test-searx.example.com c8 --reporter=text tsx __tests__/run-all.ts",
41
41
  "bootstrap": "npm install && npm run build",
42
- "inspector": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/index.js",
42
+ "inspector": "DANGEROUSLY_OMIT_AUTH=true GATEWAY_URL=http://localhost:80 npx @modelcontextprotocol/inspector node dist/index.js",
43
43
  "postversion": "TAG=$(node scripts/update-version.js | tail -1) && git add src/index.ts && git commit --amend --no-edit && git tag -f $TAG"
44
44
  },
45
45
  "dependencies": {
@@ -47,15 +47,16 @@
47
47
  "@types/cors": "^2.8.19",
48
48
  "@types/express": "^5.0.3",
49
49
  "cors": "^2.8.5",
50
+ "dotenv": "^17.2.3",
50
51
  "express": "^5.1.0",
51
52
  "node-html-markdown": "^1.3.0",
52
53
  "undici": "^6.20.1"
53
54
  },
54
55
  "devDependencies": {
55
- "mcp-evals": "^1.0.18",
56
56
  "@types/node": "^22.17.2",
57
57
  "@types/supertest": "^6.0.3",
58
58
  "c8": "^10.1.3",
59
+ "mcp-evals": "^1.0.18",
59
60
  "shx": "^0.4.0",
60
61
  "supertest": "^7.1.4",
61
62
  "tsx": "^4.20.5",