@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/types.js ADDED
@@ -0,0 +1,79 @@
1
+ export function isSearXNGWebSearchArgs(args) {
2
+ return (typeof args === "object" &&
3
+ args !== null &&
4
+ "query" in args &&
5
+ typeof args.query === "string");
6
+ }
7
+ export const WEB_SEARCH_TOOL = {
8
+ 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.",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ query: {
15
+ type: "string",
16
+ description: "The search query. This is the main input for the web search",
17
+ },
18
+ pageno: {
19
+ 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",
38
+ },
39
+ },
40
+ required: ["query"],
41
+ },
42
+ };
43
+ export const READ_URL_TOOL = {
44
+ name: "web_url_read",
45
+ description: "Read the content from an URL. " +
46
+ "Use this for further information retrieving to understand the content of each URL.",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ url: {
51
+ type: "string",
52
+ description: "URL",
53
+ },
54
+ startChar: {
55
+ type: "number",
56
+ description: "Starting character position for content extraction (default: 0)",
57
+ minimum: 0,
58
+ },
59
+ maxLength: {
60
+ type: "number",
61
+ description: "Maximum number of characters to return",
62
+ minimum: 1,
63
+ },
64
+ section: {
65
+ type: "string",
66
+ description: "Extract content under a specific heading (searches for heading text)",
67
+ },
68
+ paragraphRange: {
69
+ type: "string",
70
+ description: "Return specific paragraph ranges (e.g., '1-5', '3', '10-')",
71
+ },
72
+ readHeadings: {
73
+ type: "boolean",
74
+ description: "Return only a list of headings instead of full content",
75
+ },
76
+ },
77
+ required: ["url"],
78
+ },
79
+ };
@@ -0,0 +1,10 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ interface PaginationOptions {
3
+ startChar?: number;
4
+ maxLength?: number;
5
+ section?: string;
6
+ paragraphRange?: string;
7
+ readHeadings?: boolean;
8
+ }
9
+ export declare function fetchAndConvertToMarkdown(server: Server, url: string, timeoutMs?: number, paginationOptions?: PaginationOptions): Promise<string>;
10
+ export {};
@@ -0,0 +1,212 @@
1
+ import { createProxyAgent } from "./proxy.js";
2
+ import { logMessage } from "./logging.js";
3
+ import { urlCache } from "./cache.js";
4
+ import { createURLFormatError, createNetworkError, createServerError, createContentError, createTimeoutError, createEmptyContentWarning, createUnexpectedError } from "./error-handler.js";
5
+ function applyCharacterPagination(content, startChar = 0, maxLength) {
6
+ if (startChar >= content.length) {
7
+ return "";
8
+ }
9
+ const start = Math.max(0, startChar);
10
+ const end = maxLength ? Math.min(content.length, start + maxLength) : content.length;
11
+ return content.slice(start, end);
12
+ }
13
+ function extractSection(markdownContent, sectionHeading) {
14
+ const lines = markdownContent.split('\n');
15
+ const sectionRegex = new RegExp(`^#{1,6}\s*.*${sectionHeading}.*$`, 'i');
16
+ let startIndex = -1;
17
+ let currentLevel = 0;
18
+ // Find the section start
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ if (sectionRegex.test(line)) {
22
+ startIndex = i;
23
+ currentLevel = (line.match(/^#+/) || [''])[0].length;
24
+ break;
25
+ }
26
+ }
27
+ if (startIndex === -1) {
28
+ return "";
29
+ }
30
+ // Find the section end (next heading of same or higher level)
31
+ let endIndex = lines.length;
32
+ for (let i = startIndex + 1; i < lines.length; i++) {
33
+ const line = lines[i];
34
+ const match = line.match(/^#+/);
35
+ if (match && match[0].length <= currentLevel) {
36
+ endIndex = i;
37
+ break;
38
+ }
39
+ }
40
+ return lines.slice(startIndex, endIndex).join('\n');
41
+ }
42
+ function extractParagraphRange(markdownContent, range) {
43
+ const paragraphs = markdownContent.split('\n\n').filter(p => p.trim().length > 0);
44
+ // Parse range (e.g., "1-5", "3", "10-")
45
+ const rangeMatch = range.match(/^(\d+)(?:-(\d*))?$/);
46
+ if (!rangeMatch) {
47
+ return "";
48
+ }
49
+ const start = parseInt(rangeMatch[1]) - 1; // Convert to 0-based index
50
+ const endStr = rangeMatch[2];
51
+ if (start < 0 || start >= paragraphs.length) {
52
+ return "";
53
+ }
54
+ if (endStr === undefined) {
55
+ // Single paragraph (e.g., "3")
56
+ return paragraphs[start] || "";
57
+ }
58
+ else if (endStr === "") {
59
+ // Range to end (e.g., "10-")
60
+ return paragraphs.slice(start).join('\n\n');
61
+ }
62
+ else {
63
+ // Specific range (e.g., "1-5")
64
+ const end = parseInt(endStr);
65
+ return paragraphs.slice(start, end).join('\n\n');
66
+ }
67
+ }
68
+ function extractHeadings(markdownContent) {
69
+ const lines = markdownContent.split('\n');
70
+ const headings = lines.filter(line => /^#{1,6}\s/.test(line));
71
+ if (headings.length === 0) {
72
+ return "No headings found in the content.";
73
+ }
74
+ return headings.join('\n');
75
+ }
76
+ function applyPaginationOptions(markdownContent, options) {
77
+ let result = markdownContent;
78
+ // Apply heading extraction first if requested
79
+ if (options.readHeadings) {
80
+ return extractHeadings(result);
81
+ }
82
+ // Apply section extraction
83
+ if (options.section) {
84
+ result = extractSection(result, options.section);
85
+ if (result === "") {
86
+ return `Section "${options.section}" not found in the content.`;
87
+ }
88
+ }
89
+ // Apply paragraph range filtering
90
+ if (options.paragraphRange) {
91
+ result = extractParagraphRange(result, options.paragraphRange);
92
+ if (result === "") {
93
+ return `Paragraph range "${options.paragraphRange}" is invalid or out of bounds.`;
94
+ }
95
+ }
96
+ // Apply character-based pagination last
97
+ if (options.startChar !== undefined || options.maxLength !== undefined) {
98
+ result = applyCharacterPagination(result, options.startChar, options.maxLength);
99
+ }
100
+ return result;
101
+ }
102
+ export async function fetchAndConvertToMarkdown(server, url, timeoutMs = 10000, paginationOptions = {}) {
103
+ const startTime = Date.now();
104
+ logMessage(server, "info", `Fetching URL: ${url}`);
105
+ // Check cache first
106
+ const cachedEntry = urlCache.get(url);
107
+ if (cachedEntry) {
108
+ logMessage(server, "info", `Using cached content for URL: ${url}`);
109
+ const result = applyPaginationOptions(cachedEntry.markdownContent, paginationOptions);
110
+ const duration = Date.now() - startTime;
111
+ logMessage(server, "info", `Processed cached URL: ${url} (${result.length} chars in ${duration}ms)`);
112
+ return result;
113
+ }
114
+ // Validate URL format
115
+ let parsedUrl;
116
+ try {
117
+ parsedUrl = new URL(url);
118
+ }
119
+ catch (error) {
120
+ logMessage(server, "error", `Invalid URL format: ${url}`);
121
+ throw createURLFormatError(url);
122
+ }
123
+ // Build gateway API URL
124
+ const gatewayUrl = process.env.GATEWAY_URL || "http://115.190.91.253:80";
125
+ const gatewayApiUrl = `${gatewayUrl}/api/read/${encodeURIComponent(url)}`;
126
+ // Create an AbortController instance
127
+ const controller = new AbortController();
128
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
129
+ try {
130
+ // Prepare request options with proxy support
131
+ const requestOptions = {
132
+ signal: controller.signal,
133
+ };
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
+ let response;
141
+ try {
142
+ // Fetch the URL via gateway API with the abort signal
143
+ response = await fetch(gatewayApiUrl, requestOptions);
144
+ }
145
+ catch (error) {
146
+ const context = {
147
+ url,
148
+ gatewayUrl,
149
+ proxyAgent: !!proxyAgent,
150
+ timeout: timeoutMs
151
+ };
152
+ throw createNetworkError(error, context);
153
+ }
154
+ if (!response.ok) {
155
+ let responseBody;
156
+ try {
157
+ responseBody = await response.text();
158
+ }
159
+ catch {
160
+ responseBody = '[Could not read response body]';
161
+ }
162
+ const context = { url, gatewayUrl };
163
+ throw createServerError(response.status, response.statusText, responseBody, context);
164
+ }
165
+ // Retrieve content from gateway API (JSON response)
166
+ let markdownContent;
167
+ try {
168
+ const jsonData = await response.json();
169
+ // Gateway API returns: { content: "...", title: "...", url: "...", wordCount: N }
170
+ if (!jsonData.content) {
171
+ throw createContentError("Gateway API returned empty content field.", url);
172
+ }
173
+ markdownContent = jsonData.content;
174
+ }
175
+ catch (error) {
176
+ if (error.name === 'MCPSearXNGError') {
177
+ throw error;
178
+ }
179
+ throw createContentError(`Failed to read gateway response: ${error.message || 'Unknown error'}`, url);
180
+ }
181
+ if (!markdownContent || markdownContent.trim().length === 0) {
182
+ logMessage(server, "warning", `Empty content from gateway: ${url}`);
183
+ return createEmptyContentWarning(url, 0, "");
184
+ }
185
+ // Cache the markdown content from gateway
186
+ urlCache.set(url, "", markdownContent);
187
+ // Apply pagination options
188
+ const result = applyPaginationOptions(markdownContent, paginationOptions);
189
+ const duration = Date.now() - startTime;
190
+ logMessage(server, "info", `Successfully fetched and converted URL: ${url} (${result.length} chars in ${duration}ms)`);
191
+ return result;
192
+ }
193
+ catch (error) {
194
+ if (error.name === "AbortError") {
195
+ logMessage(server, "error", `Timeout fetching URL: ${url} (${timeoutMs}ms)`);
196
+ throw createTimeoutError(timeoutMs, url);
197
+ }
198
+ // Re-throw our enhanced errors
199
+ if (error.name === 'MCPSearXNGError') {
200
+ logMessage(server, "error", `Error fetching URL: ${url} - ${error.message}`);
201
+ throw error;
202
+ }
203
+ // Catch any unexpected errors
204
+ logMessage(server, "error", `Unexpected error fetching URL: ${url}`, error);
205
+ const context = { url };
206
+ throw createUnexpectedError(error, context);
207
+ }
208
+ finally {
209
+ // Clean up the timeout to prevent memory leaks
210
+ clearTimeout(timeoutId);
211
+ }
212
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@amplify-studio/open-mcp",
3
+ "version": "0.8.0",
4
+ "description": "Open MCP server for web search and URL reading via Gateway API",
5
+ "license": "MIT",
6
+ "author": "Amplify Studio (https://github.com/amplify-studio)",
7
+ "homepage": "https://github.com/amplify-studio/open-mcp",
8
+ "bugs": "https://github.com/amplify-studio/open-mcp/issues",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/amplify-studio/open-mcp"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "modelcontextprotocol",
16
+ "searxng",
17
+ "search",
18
+ "web-search",
19
+ "claude",
20
+ "ai",
21
+ "pagination",
22
+ "smithery",
23
+ "url-reader"
24
+ ],
25
+ "type": "module",
26
+ "bin": {
27
+ "open-mcp": "dist/index.js"
28
+ },
29
+ "main": "dist/index.js",
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc && shx chmod +x dist/*.js",
38
+ "watch": "tsc --watch",
39
+ "test": "SEARXNG_URL=https://test-searx.example.com tsx __tests__/run-all.ts",
40
+ "test:coverage": "SEARXNG_URL=https://test-searx.example.com c8 --reporter=text tsx __tests__/run-all.ts",
41
+ "bootstrap": "npm install && npm run build",
42
+ "inspector": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/index.js",
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
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "1.17.4",
47
+ "@types/cors": "^2.8.19",
48
+ "@types/express": "^5.0.3",
49
+ "cors": "^2.8.5",
50
+ "express": "^5.1.0",
51
+ "node-html-markdown": "^1.3.0",
52
+ "undici": "^6.20.1"
53
+ },
54
+ "devDependencies": {
55
+ "mcp-evals": "^1.0.18",
56
+ "@types/node": "^22.17.2",
57
+ "@types/supertest": "^6.0.3",
58
+ "c8": "^10.1.3",
59
+ "shx": "^0.4.0",
60
+ "supertest": "^7.1.4",
61
+ "tsx": "^4.20.5",
62
+ "typescript": "^5.8.3"
63
+ }
64
+ }