@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/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.js +68 -0
- package/dist/error-handler.d.ts +29 -0
- package/dist/error-handler.js +114 -0
- package/dist/http-server.d.ts +3 -0
- package/dist/http-server.js +150 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +253 -0
- package/dist/logging.d.ts +6 -0
- package/dist/logging.js +42 -0
- package/dist/proxy.d.ts +16 -0
- package/dist/proxy.js +97 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +95 -0
- package/dist/search.d.ts +2 -0
- package/dist/search.js +130 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +79 -0
- package/dist/url-reader.d.ts +10 -0
- package/dist/url-reader.js +212 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 IS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Open MCP
|
|
2
|
+
|
|
3
|
+
> An open-source MCP (Model Context Protocol) server solution for AI agent integration.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Open MCP is a comprehensive server solution that enables AI assistants (like Claude) to interact with external services through the Model Context Protocol. Currently focused on web search and content extraction, with plans to expand into a full-featured AI agent platform.
|
|
10
|
+
|
|
11
|
+
## Current Features
|
|
12
|
+
|
|
13
|
+
### 🌐 Web Search
|
|
14
|
+
- General web queries with pagination
|
|
15
|
+
- Time-based filtering (day, month, year)
|
|
16
|
+
- Language selection
|
|
17
|
+
- Safe search levels
|
|
18
|
+
|
|
19
|
+
### 📄 URL Content Reading
|
|
20
|
+
- Extract web page content as text/markdown
|
|
21
|
+
- Intelligent caching with TTL
|
|
22
|
+
- Section extraction and pagination options
|
|
23
|
+
|
|
24
|
+
## Roadmap
|
|
25
|
+
|
|
26
|
+
### Phase 1: Current (Search & Content)
|
|
27
|
+
- ✅ Web search via Gateway API
|
|
28
|
+
- ✅ URL content reading
|
|
29
|
+
- ✅ Intelligent caching
|
|
30
|
+
|
|
31
|
+
### Phase 2: Knowledge Base (RAG)
|
|
32
|
+
- 🔄 Document indexing
|
|
33
|
+
- 🔄 Semantic search
|
|
34
|
+
- 🔄 Vector storage integration
|
|
35
|
+
|
|
36
|
+
### Phase 3: Data Integration
|
|
37
|
+
- ⏳ Database connectors
|
|
38
|
+
- ⏳ API integrations
|
|
39
|
+
- ⏳ File system access
|
|
40
|
+
|
|
41
|
+
### Phase 4: Agent Framework
|
|
42
|
+
- ⏳ Tool composition
|
|
43
|
+
- ⏳ Workflow orchestration
|
|
44
|
+
- ⏳ Multi-agent coordination
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ Open MCP │
|
|
51
|
+
├─────────────────────────────────────────────────────────────┤
|
|
52
|
+
│ │
|
|
53
|
+
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
54
|
+
│ │ Search │ │ Content │ │ Future │ │
|
|
55
|
+
│ │ Module │ │ Reader │ │ Modules │ │
|
|
56
|
+
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
|
|
57
|
+
│ │ │ │ │
|
|
58
|
+
│ └────────────────┴────────────────┘ │
|
|
59
|
+
│ │ │
|
|
60
|
+
│ ┌─────▼─────┐ │
|
|
61
|
+
│ │ Core │ │
|
|
62
|
+
│ │ Layer │ │
|
|
63
|
+
│ └─────┬─────┘ │
|
|
64
|
+
│ │ │
|
|
65
|
+
│ ┌──────────────────────┼──────────────────────┐ │
|
|
66
|
+
│ │ │ │ │
|
|
67
|
+
│ ▼▼ ▼▼ ▼▼ │
|
|
68
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
|
69
|
+
│ Cache │ │ Gateway │ │ Plugins │ │
|
|
70
|
+
└─────────┘ └─────────┘ └─────────┘ │
|
|
71
|
+
│ │
|
|
72
|
+
└─────────────────────────────────────────────────────────────┘
|
|
73
|
+
│
|
|
74
|
+
▼
|
|
75
|
+
┌─────────────────┐
|
|
76
|
+
│ Gateway API │
|
|
77
|
+
│ (External) │
|
|
78
|
+
└─────────────────┘
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
### Option 1: From npm (Recommended for Users)
|
|
84
|
+
|
|
85
|
+
Install from npm registry:
|
|
86
|
+
|
|
87
|
+
**Using Claude CLI:**
|
|
88
|
+
```bash
|
|
89
|
+
claude mcp add @amplify-studio/open-mcp
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Or with npx:**
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"open-mcp": {
|
|
97
|
+
"command": "npx",
|
|
98
|
+
"args": ["-y", "@amplify-studio/open-mcp"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Option 2: From GitHub
|
|
105
|
+
|
|
106
|
+
No installation needed - runs directly from GitHub:
|
|
107
|
+
|
|
108
|
+
**Claude Desktop Config**:
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"open-mcp": {
|
|
113
|
+
"command": "npx",
|
|
114
|
+
"args": ["-y", "github:amplify-studio/open-mcp"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Option 3: Custom Gateway
|
|
121
|
+
|
|
122
|
+
If you have your own gateway instance:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"open-mcp": {
|
|
128
|
+
"command": "npx",
|
|
129
|
+
"args": ["-y", "github:amplify-studio/open-mcp"],
|
|
130
|
+
"env": {
|
|
131
|
+
"GATEWAY_URL": "http://your-gateway.com:80"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Option 4: Local Development (For Developers)
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Clone the repository
|
|
142
|
+
git clone https://github.com/amplify-studio/open-mcp.git
|
|
143
|
+
cd open-mcp
|
|
144
|
+
|
|
145
|
+
# Install dependencies
|
|
146
|
+
npm install
|
|
147
|
+
|
|
148
|
+
# Build the project
|
|
149
|
+
npm run build
|
|
150
|
+
|
|
151
|
+
# Run directly
|
|
152
|
+
node dist/index.js
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Claude Desktop Config** (local development):
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"mcpServers": {
|
|
159
|
+
"open-mcp": {
|
|
160
|
+
"command": "node",
|
|
161
|
+
"args": ["/path/to/open-mcp/dist/index.js"]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Option 5: Test with MCP Inspector
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
cd open-mcp
|
|
171
|
+
npm run inspector
|
|
172
|
+
# Visit http://localhost:6274 to test
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Option 6: Global Install (Advanced)
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cd open-mcp
|
|
179
|
+
npm link
|
|
180
|
+
# Then use: open-mcp
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Configuration
|
|
184
|
+
|
|
185
|
+
| Variable | Required | Default | Description |
|
|
186
|
+
|----------|----------|---------|-------------|
|
|
187
|
+
| `GATEWAY_URL` | No | `http://115.190.91.253:80` | Gateway API base URL |
|
|
188
|
+
| `AUTH_USERNAME` | No | - | Basic auth username |
|
|
189
|
+
| `AUTH_PASSWORD` | No | - | Basic auth password |
|
|
190
|
+
| `HTTP_PROXY` | No | - | Proxy URL for HTTP requests |
|
|
191
|
+
| `HTTPS_PROXY` | No | - | Proxy URL for HTTPS requests |
|
|
192
|
+
| `NO_PROXY` | No | - | Comma-separated bypass list |
|
|
193
|
+
| `MCP_HTTP_PORT` | No | - | Enable HTTP transport on specified port |
|
|
194
|
+
|
|
195
|
+
## Gateway API
|
|
196
|
+
|
|
197
|
+
The server connects to a Gateway API that provides:
|
|
198
|
+
|
|
199
|
+
| API | Method | Endpoint | Description |
|
|
200
|
+
|-----|--------|----------|-------------|
|
|
201
|
+
| Search | GET | `/api/search/` | Web search |
|
|
202
|
+
| Read | GET | `/api/read/{url}` | Extract web content |
|
|
203
|
+
| Health | GET | `/health` | Health check |
|
|
204
|
+
| Status | GET | `/api/status` | Service status |
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Install dependencies
|
|
210
|
+
npm install
|
|
211
|
+
|
|
212
|
+
# Development mode with file watching
|
|
213
|
+
npm run watch
|
|
214
|
+
|
|
215
|
+
# Run tests
|
|
216
|
+
npm test
|
|
217
|
+
|
|
218
|
+
# Test with MCP Inspector
|
|
219
|
+
npm run inspector
|
|
220
|
+
|
|
221
|
+
# Build for production
|
|
222
|
+
npm run build
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Contributing
|
|
226
|
+
|
|
227
|
+
We're building an open-source community for AI agent infrastructure. Contributions welcome!
|
|
228
|
+
|
|
229
|
+
- Fork the repository
|
|
230
|
+
- Create a feature branch
|
|
231
|
+
- Make your changes
|
|
232
|
+
- Submit a pull request
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT
|
|
237
|
+
|
|
238
|
+
## Credits
|
|
239
|
+
|
|
240
|
+
Based on [mcp-searxng](https://github.com/ihor-sokoliuk/mcp-searxng) by Ihor Sokoliuk
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
**Made with ❤️ by [Amplify Studio](https://github.com/amplify-studio)**
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface CacheEntry {
|
|
2
|
+
htmlContent: string;
|
|
3
|
+
markdownContent: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
}
|
|
6
|
+
declare class SimpleCache {
|
|
7
|
+
private cache;
|
|
8
|
+
private readonly ttlMs;
|
|
9
|
+
private cleanupInterval;
|
|
10
|
+
constructor(ttlMs?: number);
|
|
11
|
+
private startCleanup;
|
|
12
|
+
private cleanupExpired;
|
|
13
|
+
get(url: string): CacheEntry | null;
|
|
14
|
+
set(url: string, htmlContent: string, markdownContent: string): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
destroy(): void;
|
|
17
|
+
getStats(): {
|
|
18
|
+
size: number;
|
|
19
|
+
entries: Array<{
|
|
20
|
+
url: string;
|
|
21
|
+
age: number;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare const urlCache: SimpleCache;
|
|
26
|
+
export { SimpleCache };
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
class SimpleCache {
|
|
2
|
+
cache = new Map();
|
|
3
|
+
ttlMs;
|
|
4
|
+
cleanupInterval = null;
|
|
5
|
+
constructor(ttlMs = 60000) {
|
|
6
|
+
this.ttlMs = ttlMs;
|
|
7
|
+
this.startCleanup();
|
|
8
|
+
}
|
|
9
|
+
startCleanup() {
|
|
10
|
+
// Clean up expired entries every 30 seconds
|
|
11
|
+
this.cleanupInterval = setInterval(() => {
|
|
12
|
+
this.cleanupExpired();
|
|
13
|
+
}, 30000);
|
|
14
|
+
}
|
|
15
|
+
cleanupExpired() {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
18
|
+
if (now - entry.timestamp > this.ttlMs) {
|
|
19
|
+
this.cache.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
get(url) {
|
|
24
|
+
const entry = this.cache.get(url);
|
|
25
|
+
if (!entry) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Check if expired
|
|
29
|
+
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
30
|
+
this.cache.delete(url);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
set(url, htmlContent, markdownContent) {
|
|
36
|
+
this.cache.set(url, {
|
|
37
|
+
htmlContent,
|
|
38
|
+
markdownContent,
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.cache.clear();
|
|
44
|
+
}
|
|
45
|
+
destroy() {
|
|
46
|
+
if (this.cleanupInterval) {
|
|
47
|
+
clearInterval(this.cleanupInterval);
|
|
48
|
+
this.cleanupInterval = null;
|
|
49
|
+
}
|
|
50
|
+
this.clear();
|
|
51
|
+
}
|
|
52
|
+
// Get cache statistics for debugging
|
|
53
|
+
getStats() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const entries = Array.from(this.cache.entries()).map(([url, entry]) => ({
|
|
56
|
+
url,
|
|
57
|
+
age: now - entry.timestamp
|
|
58
|
+
}));
|
|
59
|
+
return {
|
|
60
|
+
size: this.cache.size,
|
|
61
|
+
entries
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Global cache instance
|
|
66
|
+
export const urlCache = new SimpleCache();
|
|
67
|
+
// Export for testing and cleanup
|
|
68
|
+
export { SimpleCache };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concise error handling for MCP SearXNG server
|
|
3
|
+
* Provides clear, focused error messages that identify the root cause
|
|
4
|
+
*/
|
|
5
|
+
export interface ErrorContext {
|
|
6
|
+
url?: string;
|
|
7
|
+
searxngUrl?: string;
|
|
8
|
+
gatewayUrl?: string;
|
|
9
|
+
proxyAgent?: boolean;
|
|
10
|
+
username?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
query?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class MCPSearXNGError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare function createConfigurationError(message: string): MCPSearXNGError;
|
|
18
|
+
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;
|
|
22
|
+
export declare function createNoResultsMessage(query: string): string;
|
|
23
|
+
export declare function createURLFormatError(url: string): MCPSearXNGError;
|
|
24
|
+
export declare function createContentError(message: string, url: string): MCPSearXNGError;
|
|
25
|
+
export declare function createConversionError(error: any, url: string, htmlContent: string): MCPSearXNGError;
|
|
26
|
+
export declare function createTimeoutError(timeout: number, url: string): MCPSearXNGError;
|
|
27
|
+
export declare function createEmptyContentWarning(url: string, htmlLength: number, htmlPreview: string): string;
|
|
28
|
+
export declare function createUnexpectedError(error: any, context: ErrorContext): MCPSearXNGError;
|
|
29
|
+
export declare function validateEnvironment(): string | null;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concise error handling for MCP SearXNG server
|
|
3
|
+
* Provides clear, focused error messages that identify the root cause
|
|
4
|
+
*/
|
|
5
|
+
export class MCPSearXNGError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'MCPSearXNGError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function createConfigurationError(message) {
|
|
12
|
+
return new MCPSearXNGError(`🔧 Configuration Error: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
export function createNetworkError(error, context) {
|
|
15
|
+
const target = context.searxngUrl ? 'SearXNG server' : (context.gatewayUrl ? 'Gateway server' : 'website');
|
|
16
|
+
if (error.code === 'ECONNREFUSED') {
|
|
17
|
+
return new MCPSearXNGError(`🌐 Connection Error: ${target} is not responding (${context.url})`);
|
|
18
|
+
}
|
|
19
|
+
if (error.code === 'ENOTFOUND' || error.code === 'EAI_NONAME') {
|
|
20
|
+
const hostname = context.url ? new URL(context.url).hostname : 'unknown';
|
|
21
|
+
return new MCPSearXNGError(`🌐 DNS Error: Cannot resolve hostname "${hostname}"`);
|
|
22
|
+
}
|
|
23
|
+
if (error.code === 'ETIMEDOUT') {
|
|
24
|
+
return new MCPSearXNGError(`🌐 Timeout Error: ${target} is too slow to respond`);
|
|
25
|
+
}
|
|
26
|
+
if (error.message?.includes('certificate')) {
|
|
27
|
+
return new MCPSearXNGError(`🌐 SSL Error: Certificate problem with ${target}`);
|
|
28
|
+
}
|
|
29
|
+
// For generic fetch failures, provide root cause guidance
|
|
30
|
+
const errorMsg = error.message || error.code || 'Connection failed';
|
|
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');
|
|
37
|
+
return new MCPSearXNGError(`🌐 Network Error: ${errorMsg}. ${guidance}`);
|
|
38
|
+
}
|
|
39
|
+
return new MCPSearXNGError(`🌐 Network Error: ${errorMsg}`);
|
|
40
|
+
}
|
|
41
|
+
export function createServerError(status, statusText, responseBody, context) {
|
|
42
|
+
const target = context.searxngUrl ? 'SearXNG server' : (context.gatewayUrl ? 'Gateway server' : 'Website');
|
|
43
|
+
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}`);
|
|
46
|
+
}
|
|
47
|
+
if (status === 404) {
|
|
48
|
+
const reason = context.searxngUrl ? 'Search endpoint not found' : 'Page not found';
|
|
49
|
+
return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${reason}`);
|
|
50
|
+
}
|
|
51
|
+
if (status === 429) {
|
|
52
|
+
return new MCPSearXNGError(`🚫 ${target} Error (${status}): Rate limit exceeded`);
|
|
53
|
+
}
|
|
54
|
+
if (status >= 500) {
|
|
55
|
+
return new MCPSearXNGError(`🚫 ${target} Error (${status}): Internal server error`);
|
|
56
|
+
}
|
|
57
|
+
return new MCPSearXNGError(`🚫 ${target} Error (${status}): ${statusText}`);
|
|
58
|
+
}
|
|
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
|
+
export function createNoResultsMessage(query) {
|
|
67
|
+
return `🔍 No results found for "${query}". Try different search terms or check if SearXNG search engines are working.`;
|
|
68
|
+
}
|
|
69
|
+
export function createURLFormatError(url) {
|
|
70
|
+
return new MCPSearXNGError(`🔧 URL Format Error: Invalid URL "${url}"`);
|
|
71
|
+
}
|
|
72
|
+
export function createContentError(message, url) {
|
|
73
|
+
return new MCPSearXNGError(`📄 Content Error: ${message} (${url})`);
|
|
74
|
+
}
|
|
75
|
+
export function createConversionError(error, url, htmlContent) {
|
|
76
|
+
return new MCPSearXNGError(`🔄 Conversion Error: Cannot convert HTML to Markdown (${url})`);
|
|
77
|
+
}
|
|
78
|
+
export function createTimeoutError(timeout, url) {
|
|
79
|
+
const hostname = new URL(url).hostname;
|
|
80
|
+
return new MCPSearXNGError(`⏱️ Timeout Error: ${hostname} took longer than ${timeout}ms to respond`);
|
|
81
|
+
}
|
|
82
|
+
export function createEmptyContentWarning(url, htmlLength, htmlPreview) {
|
|
83
|
+
return `📄 Content Warning: Page fetched but appears empty after conversion (${url}). May contain only media or require JavaScript.`;
|
|
84
|
+
}
|
|
85
|
+
export function createUnexpectedError(error, context) {
|
|
86
|
+
return new MCPSearXNGError(`❓ Unexpected Error: ${error.message || String(error)}`);
|
|
87
|
+
}
|
|
88
|
+
export function validateEnvironment() {
|
|
89
|
+
const issues = [];
|
|
90
|
+
const gatewayUrl = process.env.GATEWAY_URL;
|
|
91
|
+
if (gatewayUrl) {
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(gatewayUrl);
|
|
94
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
95
|
+
issues.push(`GATEWAY_URL invalid protocol: ${url.protocol}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
issues.push(`GATEWAY_URL invalid format: ${gatewayUrl}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
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");
|
|
106
|
+
}
|
|
107
|
+
else if (!authUsername && authPassword) {
|
|
108
|
+
issues.push("AUTH_PASSWORD set but AUTH_USERNAME missing");
|
|
109
|
+
}
|
|
110
|
+
if (issues.length === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return `⚠️ Configuration Issues: ${issues.join(', ')}. GATEWAY_URL is optional (defaults to http://115.190.91.253:80)`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { logMessage } from "./logging.js";
|
|
7
|
+
import { packageVersion } from "./index.js";
|
|
8
|
+
export async function createHttpServer(server) {
|
|
9
|
+
const app = express();
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
// Add CORS support for web clients
|
|
12
|
+
app.use(cors({
|
|
13
|
+
origin: '*', // Configure appropriately for production
|
|
14
|
+
exposedHeaders: ['Mcp-Session-Id'],
|
|
15
|
+
allowedHeaders: ['Content-Type', 'mcp-session-id'],
|
|
16
|
+
}));
|
|
17
|
+
// Map to store transports by session ID
|
|
18
|
+
const transports = {};
|
|
19
|
+
// Handle POST requests for client-to-server communication
|
|
20
|
+
app.post('/mcp', async (req, res) => {
|
|
21
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
22
|
+
let transport;
|
|
23
|
+
if (sessionId && transports[sessionId]) {
|
|
24
|
+
// Reuse existing transport
|
|
25
|
+
transport = transports[sessionId];
|
|
26
|
+
logMessage(server, "debug", `Reusing session: ${sessionId}`);
|
|
27
|
+
}
|
|
28
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
29
|
+
// New initialization request
|
|
30
|
+
logMessage(server, "info", "Creating new HTTP session");
|
|
31
|
+
transport = new StreamableHTTPServerTransport({
|
|
32
|
+
sessionIdGenerator: () => randomUUID(),
|
|
33
|
+
onsessioninitialized: (sessionId) => {
|
|
34
|
+
transports[sessionId] = transport;
|
|
35
|
+
logMessage(server, "debug", `Session initialized: ${sessionId}`);
|
|
36
|
+
},
|
|
37
|
+
// DNS rebinding protection disabled by default for backwards compatibility
|
|
38
|
+
// For production, consider enabling:
|
|
39
|
+
// enableDnsRebindingProtection: true,
|
|
40
|
+
// allowedHosts: ['127.0.0.1', 'localhost'],
|
|
41
|
+
});
|
|
42
|
+
// Clean up transport when closed
|
|
43
|
+
transport.onclose = () => {
|
|
44
|
+
if (transport.sessionId) {
|
|
45
|
+
logMessage(server, "debug", `Session closed: ${transport.sessionId}`);
|
|
46
|
+
delete transports[transport.sessionId];
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
// Connect the existing server to the new transport
|
|
50
|
+
await server.connect(transport);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Invalid request
|
|
54
|
+
console.warn(`⚠️ POST request rejected - invalid request:`, {
|
|
55
|
+
clientIP: req.ip || req.connection.remoteAddress,
|
|
56
|
+
sessionId: sessionId || 'undefined',
|
|
57
|
+
hasInitializeRequest: isInitializeRequest(req.body),
|
|
58
|
+
userAgent: req.headers['user-agent'],
|
|
59
|
+
contentType: req.headers['content-type'],
|
|
60
|
+
accept: req.headers['accept']
|
|
61
|
+
});
|
|
62
|
+
res.status(400).json({
|
|
63
|
+
jsonrpc: '2.0',
|
|
64
|
+
error: {
|
|
65
|
+
code: -32000,
|
|
66
|
+
message: 'Bad Request: No valid session ID provided',
|
|
67
|
+
},
|
|
68
|
+
id: null,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Handle the request
|
|
73
|
+
try {
|
|
74
|
+
await transport.handleRequest(req, res, req.body);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Log header-related rejections for debugging
|
|
78
|
+
if (error instanceof Error && error.message.includes('accept')) {
|
|
79
|
+
console.warn(`⚠️ Connection rejected due to missing headers:`, {
|
|
80
|
+
clientIP: req.ip || req.connection.remoteAddress,
|
|
81
|
+
userAgent: req.headers['user-agent'],
|
|
82
|
+
contentType: req.headers['content-type'],
|
|
83
|
+
accept: req.headers['accept'],
|
|
84
|
+
error: error.message
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Handle GET requests for server-to-client notifications via SSE
|
|
91
|
+
app.get('/mcp', async (req, res) => {
|
|
92
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
93
|
+
if (!sessionId || !transports[sessionId]) {
|
|
94
|
+
console.warn(`⚠️ GET request rejected - missing or invalid session ID:`, {
|
|
95
|
+
clientIP: req.ip || req.connection.remoteAddress,
|
|
96
|
+
sessionId: sessionId || 'undefined',
|
|
97
|
+
userAgent: req.headers['user-agent']
|
|
98
|
+
});
|
|
99
|
+
res.status(400).send('Invalid or missing session ID');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const transport = transports[sessionId];
|
|
103
|
+
try {
|
|
104
|
+
await transport.handleRequest(req, res);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.warn(`⚠️ GET request failed:`, {
|
|
108
|
+
clientIP: req.ip || req.connection.remoteAddress,
|
|
109
|
+
sessionId,
|
|
110
|
+
error: error instanceof Error ? error.message : String(error)
|
|
111
|
+
});
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// Handle DELETE requests for session termination
|
|
116
|
+
app.delete('/mcp', async (req, res) => {
|
|
117
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
118
|
+
if (!sessionId || !transports[sessionId]) {
|
|
119
|
+
console.warn(`⚠️ DELETE request rejected - missing or invalid session ID:`, {
|
|
120
|
+
clientIP: req.ip || req.connection.remoteAddress,
|
|
121
|
+
sessionId: sessionId || 'undefined',
|
|
122
|
+
userAgent: req.headers['user-agent']
|
|
123
|
+
});
|
|
124
|
+
res.status(400).send('Invalid or missing session ID');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const transport = transports[sessionId];
|
|
128
|
+
try {
|
|
129
|
+
await transport.handleRequest(req, res);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.warn(`⚠️ DELETE request failed:`, {
|
|
133
|
+
clientIP: req.ip || req.connection.remoteAddress,
|
|
134
|
+
sessionId,
|
|
135
|
+
error: error instanceof Error ? error.message : String(error)
|
|
136
|
+
});
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Health check endpoint
|
|
141
|
+
app.get('/health', (_req, res) => {
|
|
142
|
+
res.json({
|
|
143
|
+
status: 'healthy',
|
|
144
|
+
server: 'ihor-sokoliuk/mcp-searxng',
|
|
145
|
+
version: packageVersion,
|
|
146
|
+
transport: 'http'
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return app;
|
|
150
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
declare const packageVersion = "0.8.0";
|
|
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
|
+
};
|