@kassol/mcp-searxng 1.0.3-custom.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 +255 -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 +148 -0
- package/dist/headers.d.ts +3 -0
- package/dist/headers.js +77 -0
- package/dist/http-security.d.ts +15 -0
- package/dist/http-security.js +52 -0
- package/dist/http-server.d.ts +3 -0
- package/dist/http-server.js +185 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +252 -0
- package/dist/logging.d.ts +6 -0
- package/dist/logging.js +35 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.js +215 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +114 -0
- package/dist/search.d.ts +2 -0
- package/dist/search.js +133 -0
- package/dist/tls-config.d.ts +19 -0
- package/dist/tls-config.js +49 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +87 -0
- package/dist/url-reader.d.ts +10 -0
- package/dist/url-reader.js +276 -0
- package/package.json +72 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function isEnabled(value) {
|
|
2
|
+
return value === "true";
|
|
3
|
+
}
|
|
4
|
+
function parseCsv(value) {
|
|
5
|
+
return (value || "")
|
|
6
|
+
.split(",")
|
|
7
|
+
.map((item) => item.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
export function getHttpSecurityConfig() {
|
|
11
|
+
const harden = isEnabled(process.env.MCP_HTTP_HARDEN);
|
|
12
|
+
const authToken = process.env.MCP_HTTP_AUTH_TOKEN;
|
|
13
|
+
const allowedOrigins = parseCsv(process.env.MCP_HTTP_ALLOWED_ORIGINS);
|
|
14
|
+
const allowedHosts = parseCsv(process.env.MCP_HTTP_ALLOWED_HOSTS);
|
|
15
|
+
return {
|
|
16
|
+
harden,
|
|
17
|
+
requireAuth: harden,
|
|
18
|
+
authToken,
|
|
19
|
+
restrictOrigins: harden,
|
|
20
|
+
allowedOrigins,
|
|
21
|
+
enableDnsRebindingProtection: harden,
|
|
22
|
+
allowedHosts: allowedHosts.length > 0 ? allowedHosts : ["127.0.0.1", "localhost"],
|
|
23
|
+
exposeFullConfig: isEnabled(process.env.MCP_HTTP_EXPOSE_FULL_CONFIG),
|
|
24
|
+
allowPrivateUrls: isEnabled(process.env.MCP_HTTP_ALLOW_PRIVATE_URLS),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function validateHttpSecurityConfig(config) {
|
|
28
|
+
if (!config.harden) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!config.authToken) {
|
|
32
|
+
throw new Error("MCP_HTTP_HARDEN=true requires MCP_HTTP_AUTH_TOKEN to be set.");
|
|
33
|
+
}
|
|
34
|
+
if (config.allowedOrigins.length === 0) {
|
|
35
|
+
throw new Error("MCP_HTTP_HARDEN=true requires MCP_HTTP_ALLOWED_ORIGINS to be set.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function isRequestAuthorized(headerValue, config) {
|
|
39
|
+
if (!config.requireAuth) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return headerValue === `Bearer ${config.authToken}` || headerValue === config.authToken;
|
|
43
|
+
}
|
|
44
|
+
export function isOriginAllowed(origin, config) {
|
|
45
|
+
if (!config.restrictOrigins) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (!origin) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return config.allowedOrigins.includes(origin);
|
|
52
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
import { getHttpSecurityConfig, isOriginAllowed, isRequestAuthorized, validateHttpSecurityConfig, } from "./http-security.js";
|
|
9
|
+
export async function createHttpServer(createMcpServer) {
|
|
10
|
+
const app = express();
|
|
11
|
+
const security = getHttpSecurityConfig();
|
|
12
|
+
validateHttpSecurityConfig(security);
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
// Add CORS support for web clients
|
|
15
|
+
app.use(cors({
|
|
16
|
+
origin: (origin, callback) => {
|
|
17
|
+
if (isOriginAllowed(origin || undefined, security)) {
|
|
18
|
+
callback(null, true);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
callback(null, false);
|
|
22
|
+
},
|
|
23
|
+
exposedHeaders: ["Mcp-Session-Id"],
|
|
24
|
+
allowedHeaders: ["Content-Type", "mcp-session-id", "authorization"],
|
|
25
|
+
}));
|
|
26
|
+
function rejectUnauthorized(res) {
|
|
27
|
+
res.status(401).json({
|
|
28
|
+
jsonrpc: "2.0",
|
|
29
|
+
error: {
|
|
30
|
+
code: -32001,
|
|
31
|
+
message: "Unauthorized: missing or invalid HTTP auth token",
|
|
32
|
+
},
|
|
33
|
+
id: null,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Map to store sessions by session ID
|
|
37
|
+
const sessions = new Map();
|
|
38
|
+
// Handle POST requests for client-to-server communication
|
|
39
|
+
app.post('/mcp', async (req, res) => {
|
|
40
|
+
if (!isRequestAuthorized(req.headers.authorization, security)) {
|
|
41
|
+
rejectUnauthorized(res);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
45
|
+
let transport;
|
|
46
|
+
let mcpServer;
|
|
47
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
48
|
+
// Reuse existing session
|
|
49
|
+
const session = sessions.get(sessionId);
|
|
50
|
+
transport = session.transport;
|
|
51
|
+
mcpServer = session.mcpServer;
|
|
52
|
+
logMessage(mcpServer, "debug", `Reusing session: ${sessionId}`);
|
|
53
|
+
}
|
|
54
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
55
|
+
// New initialization request — create fresh McpServer and transport
|
|
56
|
+
mcpServer = createMcpServer();
|
|
57
|
+
transport = new StreamableHTTPServerTransport({
|
|
58
|
+
sessionIdGenerator: () => randomUUID(),
|
|
59
|
+
onsessioninitialized: (sessionId) => {
|
|
60
|
+
sessions.set(sessionId, { transport, mcpServer });
|
|
61
|
+
logMessage(mcpServer, "debug", `Session initialized: ${sessionId}`);
|
|
62
|
+
},
|
|
63
|
+
enableDnsRebindingProtection: security.enableDnsRebindingProtection,
|
|
64
|
+
allowedHosts: security.allowedHosts,
|
|
65
|
+
allowedOrigins: security.allowedOrigins,
|
|
66
|
+
});
|
|
67
|
+
// Clean up session when transport closes
|
|
68
|
+
transport.onclose = () => {
|
|
69
|
+
if (transport.sessionId) {
|
|
70
|
+
sessions.delete(transport.sessionId);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
// Connect this session's McpServer to its transport
|
|
74
|
+
await mcpServer.connect(transport);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Invalid request
|
|
78
|
+
console.warn(`⚠️ POST request rejected - invalid request:`, {
|
|
79
|
+
clientIP: req.ip || req.socket.remoteAddress,
|
|
80
|
+
sessionId: sessionId || 'undefined',
|
|
81
|
+
hasInitializeRequest: isInitializeRequest(req.body),
|
|
82
|
+
userAgent: req.headers['user-agent'],
|
|
83
|
+
contentType: req.headers['content-type'],
|
|
84
|
+
accept: req.headers['accept']
|
|
85
|
+
});
|
|
86
|
+
res.status(400).json({
|
|
87
|
+
jsonrpc: '2.0',
|
|
88
|
+
error: {
|
|
89
|
+
code: -32000,
|
|
90
|
+
message: 'Bad Request: No valid session ID provided',
|
|
91
|
+
},
|
|
92
|
+
id: null,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Handle the request
|
|
97
|
+
try {
|
|
98
|
+
await transport.handleRequest(req, res, req.body);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Log header-related rejections for debugging
|
|
102
|
+
if (error instanceof Error && error.message.includes('accept')) {
|
|
103
|
+
console.warn(`⚠️ Connection rejected due to missing headers:`, {
|
|
104
|
+
clientIP: req.ip || req.socket.remoteAddress,
|
|
105
|
+
userAgent: req.headers['user-agent'],
|
|
106
|
+
contentType: req.headers['content-type'],
|
|
107
|
+
accept: req.headers['accept'],
|
|
108
|
+
error: error.message
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// Handle GET requests for server-to-client notifications via SSE
|
|
115
|
+
app.get('/mcp', async (req, res) => {
|
|
116
|
+
if (!isRequestAuthorized(req.headers.authorization, security)) {
|
|
117
|
+
rejectUnauthorized(res);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
121
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
122
|
+
console.warn(`⚠️ GET request rejected - missing or invalid session ID:`, {
|
|
123
|
+
clientIP: req.ip || req.socket.remoteAddress,
|
|
124
|
+
sessionId: sessionId || 'undefined',
|
|
125
|
+
userAgent: req.headers['user-agent']
|
|
126
|
+
});
|
|
127
|
+
res.status(400).send('Invalid or missing session ID');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const session = sessions.get(sessionId);
|
|
131
|
+
try {
|
|
132
|
+
await session.transport.handleRequest(req, res);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.warn(`⚠️ GET request failed:`, {
|
|
136
|
+
clientIP: req.ip || req.socket.remoteAddress,
|
|
137
|
+
sessionId,
|
|
138
|
+
error: error instanceof Error ? error.message : String(error)
|
|
139
|
+
});
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Handle DELETE requests for session termination
|
|
144
|
+
app.delete('/mcp', async (req, res) => {
|
|
145
|
+
if (!isRequestAuthorized(req.headers.authorization, security)) {
|
|
146
|
+
rejectUnauthorized(res);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
150
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
151
|
+
console.warn(`⚠️ DELETE request rejected - missing or invalid session ID:`, {
|
|
152
|
+
clientIP: req.ip || req.socket.remoteAddress,
|
|
153
|
+
sessionId: sessionId || 'undefined',
|
|
154
|
+
userAgent: req.headers['user-agent']
|
|
155
|
+
});
|
|
156
|
+
res.status(400).send('Invalid or missing session ID');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const session = sessions.get(sessionId);
|
|
160
|
+
try {
|
|
161
|
+
await session.transport.handleRequest(req, res);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.warn(`⚠️ DELETE request failed:`, {
|
|
165
|
+
clientIP: req.ip || req.socket.remoteAddress,
|
|
166
|
+
sessionId,
|
|
167
|
+
error: error instanceof Error ? error.message : String(error)
|
|
168
|
+
});
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
sessions.delete(sessionId);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// Health check endpoint
|
|
176
|
+
app.get('/health', (_req, res) => {
|
|
177
|
+
res.json({
|
|
178
|
+
status: 'healthy',
|
|
179
|
+
server: 'kassol/mcp-searxng',
|
|
180
|
+
version: packageVersion,
|
|
181
|
+
transport: 'http'
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
return app;
|
|
185
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
declare const packageVersion = "1.0.3-custom.0";
|
|
4
|
+
export { packageVersion };
|
|
5
|
+
export declare function isWebUrlReadArgs(args: unknown): args is {
|
|
6
|
+
url: string;
|
|
7
|
+
startChar?: number;
|
|
8
|
+
maxLength?: number;
|
|
9
|
+
section?: string;
|
|
10
|
+
paragraphRange?: string;
|
|
11
|
+
readHeadings?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Creates and configures a new McpServer with all handlers registered.
|
|
15
|
+
* Called once per HTTP session, or once for STDIO mode.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createMcpServer(): McpServer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
// Import modularized functionality
|
|
6
|
+
import { WEB_SEARCH_TOOL, READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
|
|
7
|
+
import { logMessage, setLogLevel, getCurrentLogLevel } from "./logging.js";
|
|
8
|
+
import { performWebSearch } from "./search.js";
|
|
9
|
+
import { fetchAndConvertToMarkdown } from "./url-reader.js";
|
|
10
|
+
import { createConfigResource, createHelpResource } from "./resources.js";
|
|
11
|
+
import { createHttpServer } from "./http-server.js";
|
|
12
|
+
// Use a static version string that will be updated by the version script
|
|
13
|
+
const packageVersion = "1.0.3-custom.0";
|
|
14
|
+
// Export the version for use in other modules
|
|
15
|
+
export { packageVersion };
|
|
16
|
+
// Type guard for URL reading args
|
|
17
|
+
export function isWebUrlReadArgs(args) {
|
|
18
|
+
if (typeof args !== "object" ||
|
|
19
|
+
args === null ||
|
|
20
|
+
!("url" in args) ||
|
|
21
|
+
typeof args.url !== "string") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const urlArgs = args;
|
|
25
|
+
// Convert empty strings to undefined for optional string parameters
|
|
26
|
+
if (urlArgs.section === "")
|
|
27
|
+
urlArgs.section = undefined;
|
|
28
|
+
if (urlArgs.paragraphRange === "")
|
|
29
|
+
urlArgs.paragraphRange = undefined;
|
|
30
|
+
// Validate optional parameters
|
|
31
|
+
if (urlArgs.startChar !== undefined && (typeof urlArgs.startChar !== "number" || urlArgs.startChar < 0)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (urlArgs.maxLength !== undefined && (typeof urlArgs.maxLength !== "number" || urlArgs.maxLength < 1)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (urlArgs.section !== undefined && typeof urlArgs.section !== "string") {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (urlArgs.paragraphRange !== undefined && typeof urlArgs.paragraphRange !== "string") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (urlArgs.readHeadings !== undefined && typeof urlArgs.readHeadings !== "boolean") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates and configures a new McpServer with all handlers registered.
|
|
50
|
+
* Called once per HTTP session, or once for STDIO mode.
|
|
51
|
+
*/
|
|
52
|
+
export function createMcpServer() {
|
|
53
|
+
const mcpServer = new McpServer({
|
|
54
|
+
name: "kassol/mcp-searxng",
|
|
55
|
+
version: packageVersion,
|
|
56
|
+
}, {
|
|
57
|
+
capabilities: {
|
|
58
|
+
logging: {},
|
|
59
|
+
resources: {},
|
|
60
|
+
tools: {},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const server = mcpServer.server;
|
|
64
|
+
// List tools handler
|
|
65
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
66
|
+
logMessage(mcpServer, "debug", "Handling list_tools request");
|
|
67
|
+
return {
|
|
68
|
+
tools: [WEB_SEARCH_TOOL, READ_URL_TOOL],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
// Call tool handler
|
|
72
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
73
|
+
const { name, arguments: args } = request.params;
|
|
74
|
+
logMessage(mcpServer, "debug", `Handling call_tool request: ${name}`);
|
|
75
|
+
try {
|
|
76
|
+
if (name === "searxng_web_search") {
|
|
77
|
+
if (!isSearXNGWebSearchArgs(args)) {
|
|
78
|
+
throw new Error("Invalid arguments for web search");
|
|
79
|
+
}
|
|
80
|
+
const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch);
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: result,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else if (name === "web_url_read") {
|
|
91
|
+
if (!isWebUrlReadArgs(args)) {
|
|
92
|
+
throw new Error("Invalid arguments for URL reading");
|
|
93
|
+
}
|
|
94
|
+
const paginationOptions = {
|
|
95
|
+
startChar: args.startChar,
|
|
96
|
+
maxLength: args.maxLength,
|
|
97
|
+
section: args.section,
|
|
98
|
+
paragraphRange: args.paragraphRange,
|
|
99
|
+
readHeadings: args.readHeadings,
|
|
100
|
+
};
|
|
101
|
+
const result = await fetchAndConvertToMarkdown(mcpServer, args.url, 10000, paginationOptions);
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: result,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
logMessage(mcpServer, "error", `Tool execution error: ${error instanceof Error ? error.message : String(error)}`, {
|
|
117
|
+
tool: name,
|
|
118
|
+
args: args,
|
|
119
|
+
error: error instanceof Error ? error.stack : String(error)
|
|
120
|
+
});
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Logging level handler
|
|
125
|
+
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
126
|
+
const { level } = request.params;
|
|
127
|
+
logMessage(mcpServer, "info", `Setting log level to: ${level}`);
|
|
128
|
+
setLogLevel(level);
|
|
129
|
+
return {};
|
|
130
|
+
});
|
|
131
|
+
// List resources handler
|
|
132
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
133
|
+
logMessage(mcpServer, "debug", "Handling list_resources request");
|
|
134
|
+
return {
|
|
135
|
+
resources: [
|
|
136
|
+
{
|
|
137
|
+
uri: "config://server-config",
|
|
138
|
+
mimeType: "application/json",
|
|
139
|
+
name: "Server Configuration",
|
|
140
|
+
description: "Current server configuration and environment variables"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
uri: "help://usage-guide",
|
|
144
|
+
mimeType: "text/markdown",
|
|
145
|
+
name: "Usage Guide",
|
|
146
|
+
description: "How to use the MCP SearXNG server effectively"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
// List resource templates handler
|
|
152
|
+
// Returns empty list — required by MCP spec even when no templates exist
|
|
153
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
154
|
+
logMessage(mcpServer, "debug", "Handling list_resource_templates request");
|
|
155
|
+
return { resourceTemplates: [] };
|
|
156
|
+
});
|
|
157
|
+
// Read resource handler
|
|
158
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
159
|
+
const { uri } = request.params;
|
|
160
|
+
logMessage(mcpServer, "debug", `Handling read_resource request for: ${uri}`);
|
|
161
|
+
switch (uri) {
|
|
162
|
+
case "config://server-config":
|
|
163
|
+
return {
|
|
164
|
+
contents: [
|
|
165
|
+
{
|
|
166
|
+
uri: uri,
|
|
167
|
+
mimeType: "application/json",
|
|
168
|
+
text: createConfigResource()
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
case "help://usage-guide":
|
|
173
|
+
return {
|
|
174
|
+
contents: [
|
|
175
|
+
{
|
|
176
|
+
uri: uri,
|
|
177
|
+
mimeType: "text/markdown",
|
|
178
|
+
text: createHelpResource()
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
default:
|
|
183
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return mcpServer;
|
|
187
|
+
}
|
|
188
|
+
// Main function
|
|
189
|
+
async function main() {
|
|
190
|
+
// Check for HTTP transport mode
|
|
191
|
+
const httpPort = process.env.MCP_HTTP_PORT;
|
|
192
|
+
if (httpPort) {
|
|
193
|
+
const port = parseInt(httpPort, 10);
|
|
194
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
195
|
+
console.error(`Invalid HTTP port: ${httpPort}. Must be between 1-65535.`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
console.log(`Starting HTTP transport on port ${port}`);
|
|
199
|
+
const app = await createHttpServer(createMcpServer);
|
|
200
|
+
const httpServer = app.listen(port, () => {
|
|
201
|
+
console.log(`HTTP server listening on port ${port}`);
|
|
202
|
+
console.log(`Health check: http://localhost:${port}/health`);
|
|
203
|
+
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
|
204
|
+
});
|
|
205
|
+
// Handle graceful shutdown
|
|
206
|
+
const shutdown = (signal) => {
|
|
207
|
+
console.log(`Received ${signal}. Shutting down HTTP server...`);
|
|
208
|
+
httpServer.close(() => {
|
|
209
|
+
console.log("HTTP server closed");
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
214
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Default STDIO transport — single session, single server
|
|
218
|
+
const mcpServer = createMcpServer();
|
|
219
|
+
// Show helpful message when running in terminal
|
|
220
|
+
if (process.stdin.isTTY) {
|
|
221
|
+
console.error(`🔍 MCP SearXNG Server v${packageVersion} - Ready`);
|
|
222
|
+
if (process.env.SEARXNG_URL) {
|
|
223
|
+
console.error(`🌐 SearXNG URL: ${process.env.SEARXNG_URL}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.error("⚠️ SEARXNG_URL not set — configure it before using search tools");
|
|
227
|
+
}
|
|
228
|
+
console.error("📡 Waiting for MCP client connection via STDIO...\n");
|
|
229
|
+
}
|
|
230
|
+
const transport = new StdioServerTransport();
|
|
231
|
+
await mcpServer.connect(transport);
|
|
232
|
+
// Log after connection is established
|
|
233
|
+
logMessage(mcpServer, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
|
|
234
|
+
logMessage(mcpServer, "info", `Log level: ${getCurrentLogLevel()}`);
|
|
235
|
+
logMessage(mcpServer, "info", `Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
236
|
+
logMessage(mcpServer, "info", `SearXNG URL: ${process.env.SEARXNG_URL || 'not configured'}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Handle uncaught errors
|
|
240
|
+
process.on('uncaughtException', (error) => {
|
|
241
|
+
console.error('Uncaught Exception:', error);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
});
|
|
244
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
245
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
// Start the server (CLI entrypoint)
|
|
249
|
+
main().catch((error) => {
|
|
250
|
+
console.error("Failed to start server:", error);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare function logMessage(mcpServer: McpServer, level: LoggingLevel, message: string, data?: unknown): void;
|
|
4
|
+
export declare function shouldLog(level: LoggingLevel): boolean;
|
|
5
|
+
export declare function setLogLevel(level: LoggingLevel): void;
|
|
6
|
+
export declare function getCurrentLogLevel(): LoggingLevel;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Logging state
|
|
2
|
+
let currentLogLevel = "info";
|
|
3
|
+
// Shared handler for sendLoggingMessage errors
|
|
4
|
+
function handleSendError(error) {
|
|
5
|
+
if (error instanceof Error && error.message !== "Not connected") {
|
|
6
|
+
console.error("Logging error:", error);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
// Logging helper function
|
|
10
|
+
export function logMessage(mcpServer, level, message, data) {
|
|
11
|
+
if (shouldLog(level)) {
|
|
12
|
+
try {
|
|
13
|
+
const notificationData = data !== undefined
|
|
14
|
+
? (typeof data === 'object' && data !== null ? { message, ...data } : { message, data })
|
|
15
|
+
: { message };
|
|
16
|
+
mcpServer.sendLoggingMessage({
|
|
17
|
+
level,
|
|
18
|
+
data: notificationData
|
|
19
|
+
}).catch(handleSendError);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
handleSendError(error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function shouldLog(level) {
|
|
27
|
+
const levels = ["debug", "info", "warning", "error"];
|
|
28
|
+
return levels.indexOf(level) >= levels.indexOf(currentLogLevel);
|
|
29
|
+
}
|
|
30
|
+
export function setLogLevel(level) {
|
|
31
|
+
currentLogLevel = level;
|
|
32
|
+
}
|
|
33
|
+
export function getCurrentLogLevel() {
|
|
34
|
+
return currentLogLevel;
|
|
35
|
+
}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Agent, ProxyAgent } from "undici";
|
|
2
|
+
/**
|
|
3
|
+
* Proxy configuration type for separating search and URL reader proxies.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ProxyType: {
|
|
6
|
+
readonly SEARCH: "search";
|
|
7
|
+
readonly URL_READER: "url_reader";
|
|
8
|
+
};
|
|
9
|
+
export type ProxyType = typeof ProxyType[keyof typeof ProxyType];
|
|
10
|
+
/**
|
|
11
|
+
* Creates a proxy agent dispatcher for Node.js fetch API.
|
|
12
|
+
*
|
|
13
|
+
* Node.js fetch uses Undici under the hood, which requires a 'dispatcher' option
|
|
14
|
+
* instead of 'agent'. This function creates a ProxyAgent compatible with fetch.
|
|
15
|
+
*
|
|
16
|
+
* Environment variables checked (in order, depending on URL protocol):
|
|
17
|
+
* - For type 'search' and HTTPS URLs:
|
|
18
|
+
* SEARCH_HTTPS_PROXY, SEARCH_HTTP_PROXY, search_https_proxy, search_http_proxy,
|
|
19
|
+
* then HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
|
|
20
|
+
* - For type 'search' and HTTP/unknown URLs:
|
|
21
|
+
* SEARCH_HTTP_PROXY, SEARCH_HTTPS_PROXY, search_http_proxy, search_https_proxy,
|
|
22
|
+
* then HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
|
|
23
|
+
* - For type 'url_reader' and HTTPS URLs:
|
|
24
|
+
* URL_READER_HTTPS_PROXY, URL_READER_HTTP_PROXY, url_reader_https_proxy, url_reader_http_proxy,
|
|
25
|
+
* then HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
|
|
26
|
+
* - For type 'url_reader' and HTTP/unknown URLs:
|
|
27
|
+
* URL_READER_HTTP_PROXY, URL_READER_HTTPS_PROXY, url_reader_http_proxy, url_reader_https_proxy,
|
|
28
|
+
* then HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
|
|
29
|
+
* - For no specific type and HTTPS URLs:
|
|
30
|
+
* HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
|
|
31
|
+
* - For no specific type and HTTP/unknown URLs:
|
|
32
|
+
* HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy
|
|
33
|
+
* - NO_PROXY / no_proxy: Comma-separated list of hosts to bypass proxy
|
|
34
|
+
*
|
|
35
|
+
* @param targetUrl - Optional target URL to check against NO_PROXY rules
|
|
36
|
+
* @param type - Optional proxy type ('search' or 'url_reader') for separate proxy configs
|
|
37
|
+
* @returns ProxyAgent dispatcher for fetch, or undefined if no proxy configured or bypassed
|
|
38
|
+
*/
|
|
39
|
+
export declare function createProxyAgent(targetUrl?: string, type?: ProxyType): ProxyAgent | undefined;
|
|
40
|
+
export declare function createDefaultAgent(): Agent | undefined;
|