mcp-searxng 1.0.2 → 1.0.3

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.
@@ -1,3 +1,3 @@
1
1
  import express from "express";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- export declare function createHttpServer(mcpServer: McpServer): Promise<express.Application>;
3
+ export declare function createHttpServer(createMcpServer: () => McpServer): Promise<express.Application>;
@@ -6,7 +6,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { logMessage } from "./logging.js";
7
7
  import { packageVersion } from "./index.js";
8
8
  import { getHttpSecurityConfig, isOriginAllowed, isRequestAuthorized, validateHttpSecurityConfig, } from "./http-security.js";
9
- export async function createHttpServer(mcpServer) {
9
+ export async function createHttpServer(createMcpServer) {
10
10
  const app = express();
11
11
  const security = getHttpSecurityConfig();
12
12
  validateHttpSecurityConfig(security);
@@ -33,8 +33,8 @@ export async function createHttpServer(mcpServer) {
33
33
  id: null,
34
34
  });
35
35
  }
36
- // Map to store transports by session ID
37
- const transports = {};
36
+ // Map to store sessions by session ID
37
+ const sessions = new Map();
38
38
  // Handle POST requests for client-to-server communication
39
39
  app.post('/mcp', async (req, res) => {
40
40
  if (!isRequestAuthorized(req.headers.authorization, security)) {
@@ -43,32 +43,34 @@ export async function createHttpServer(mcpServer) {
43
43
  }
44
44
  const sessionId = req.headers['mcp-session-id'];
45
45
  let transport;
46
- if (sessionId && transports[sessionId]) {
47
- // Reuse existing transport
48
- transport = transports[sessionId];
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;
49
52
  logMessage(mcpServer, "debug", `Reusing session: ${sessionId}`);
50
53
  }
51
54
  else if (!sessionId && isInitializeRequest(req.body)) {
52
- // New initialization request
53
- logMessage(mcpServer, "info", "Creating new HTTP session");
55
+ // New initialization request — create fresh McpServer and transport
56
+ mcpServer = createMcpServer();
54
57
  transport = new StreamableHTTPServerTransport({
55
58
  sessionIdGenerator: () => randomUUID(),
56
59
  onsessioninitialized: (sessionId) => {
57
- transports[sessionId] = transport;
60
+ sessions.set(sessionId, { transport, mcpServer });
58
61
  logMessage(mcpServer, "debug", `Session initialized: ${sessionId}`);
59
62
  },
60
63
  enableDnsRebindingProtection: security.enableDnsRebindingProtection,
61
64
  allowedHosts: security.allowedHosts,
62
65
  allowedOrigins: security.allowedOrigins,
63
66
  });
64
- // Clean up transport when closed
67
+ // Clean up session when transport closes
65
68
  transport.onclose = () => {
66
69
  if (transport.sessionId) {
67
- logMessage(mcpServer, "debug", `Session closed: ${transport.sessionId}`);
68
- delete transports[transport.sessionId];
70
+ sessions.delete(transport.sessionId);
69
71
  }
70
72
  };
71
- // Connect the existing server to the new transport
73
+ // Connect this session's McpServer to its transport
72
74
  await mcpServer.connect(transport);
73
75
  }
74
76
  else {
@@ -116,7 +118,7 @@ export async function createHttpServer(mcpServer) {
116
118
  return;
117
119
  }
118
120
  const sessionId = req.headers['mcp-session-id'];
119
- if (!sessionId || !transports[sessionId]) {
121
+ if (!sessionId || !sessions.has(sessionId)) {
120
122
  console.warn(`⚠️ GET request rejected - missing or invalid session ID:`, {
121
123
  clientIP: req.ip || req.socket.remoteAddress,
122
124
  sessionId: sessionId || 'undefined',
@@ -125,9 +127,9 @@ export async function createHttpServer(mcpServer) {
125
127
  res.status(400).send('Invalid or missing session ID');
126
128
  return;
127
129
  }
128
- const transport = transports[sessionId];
130
+ const session = sessions.get(sessionId);
129
131
  try {
130
- await transport.handleRequest(req, res);
132
+ await session.transport.handleRequest(req, res);
131
133
  }
132
134
  catch (error) {
133
135
  console.warn(`⚠️ GET request failed:`, {
@@ -145,7 +147,7 @@ export async function createHttpServer(mcpServer) {
145
147
  return;
146
148
  }
147
149
  const sessionId = req.headers['mcp-session-id'];
148
- if (!sessionId || !transports[sessionId]) {
150
+ if (!sessionId || !sessions.has(sessionId)) {
149
151
  console.warn(`⚠️ DELETE request rejected - missing or invalid session ID:`, {
150
152
  clientIP: req.ip || req.socket.remoteAddress,
151
153
  sessionId: sessionId || 'undefined',
@@ -154,9 +156,9 @@ export async function createHttpServer(mcpServer) {
154
156
  res.status(400).send('Invalid or missing session ID');
155
157
  return;
156
158
  }
157
- const transport = transports[sessionId];
159
+ const session = sessions.get(sessionId);
158
160
  try {
159
- await transport.handleRequest(req, res);
161
+ await session.transport.handleRequest(req, res);
160
162
  }
161
163
  catch (error) {
162
164
  console.warn(`⚠️ DELETE request failed:`, {
@@ -166,6 +168,9 @@ export async function createHttpServer(mcpServer) {
166
168
  });
167
169
  throw error;
168
170
  }
171
+ finally {
172
+ sessions.delete(sessionId);
173
+ }
169
174
  });
170
175
  // Health check endpoint
171
176
  app.get('/health', (_req, res) => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- declare const packageVersion = "1.0.2";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ declare const packageVersion = "1.0.3";
3
4
  export { packageVersion };
4
5
  export declare function isWebUrlReadArgs(args: unknown): args is {
5
6
  url: string;
@@ -9,3 +10,8 @@ export declare function isWebUrlReadArgs(args: unknown): args is {
9
10
  paragraphRange?: string;
10
11
  readHeadings?: boolean;
11
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 CHANGED
@@ -4,17 +4,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  // Import modularized functionality
6
6
  import { WEB_SEARCH_TOOL, READ_URL_TOOL, isSearXNGWebSearchArgs } from "./types.js";
7
- import { logMessage, setLogLevel } from "./logging.js";
7
+ import { logMessage, setLogLevel, getCurrentLogLevel } from "./logging.js";
8
8
  import { performWebSearch } from "./search.js";
9
9
  import { fetchAndConvertToMarkdown } from "./url-reader.js";
10
10
  import { createConfigResource, createHelpResource } from "./resources.js";
11
11
  import { createHttpServer } from "./http-server.js";
12
12
  // Use a static version string that will be updated by the version script
13
- const packageVersion = "1.0.2";
13
+ const packageVersion = "1.0.3";
14
14
  // Export the version for use in other modules
15
15
  export { packageVersion };
16
- // Global state for logging level
17
- let currentLogLevel = "info";
18
16
  // Type guard for URL reading args
19
17
  export function isWebUrlReadArgs(args) {
20
18
  if (typeof args !== "object" ||
@@ -47,142 +45,146 @@ export function isWebUrlReadArgs(args) {
47
45
  }
48
46
  return true;
49
47
  }
50
- // Server implementation
51
- const mcpServer = new McpServer({
52
- name: "ihor-sokoliuk/mcp-searxng",
53
- version: packageVersion,
54
- }, {
55
- capabilities: {
56
- logging: {},
57
- resources: {},
58
- tools: {},
59
- },
60
- });
61
- // Underlying low-level server for handler registration and passing to modules
62
- const server = mcpServer.server;
63
- // List tools handler
64
- server.setRequestHandler(ListToolsRequestSchema, async () => {
65
- logMessage(mcpServer, "debug", "Handling list_tools request");
66
- return {
67
- tools: [WEB_SEARCH_TOOL, READ_URL_TOOL],
68
- };
69
- });
70
- // Call tool handler
71
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
- const { name, arguments: args } = request.params;
73
- logMessage(mcpServer, "debug", `Handling call_tool request: ${name}`);
74
- try {
75
- if (name === "searxng_web_search") {
76
- if (!isSearXNGWebSearchArgs(args)) {
77
- throw new Error("Invalid arguments for web search");
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: "ihor-sokoliuk/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
+ };
78
89
  }
79
- const result = await performWebSearch(mcpServer, args.query, args.pageno, args.time_range, args.language, args.safesearch);
80
- return {
81
- content: [
82
- {
83
- type: "text",
84
- text: result,
85
- },
86
- ],
87
- };
88
- }
89
- else if (name === "web_url_read") {
90
- if (!isWebUrlReadArgs(args)) {
91
- throw new Error("Invalid arguments for URL reading");
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}`);
92
113
  }
93
- const paginationOptions = {
94
- startChar: args.startChar,
95
- maxLength: args.maxLength,
96
- section: args.section,
97
- paragraphRange: args.paragraphRange,
98
- readHeadings: args.readHeadings,
99
- };
100
- const result = await fetchAndConvertToMarkdown(mcpServer, args.url, 10000, paginationOptions);
101
- return {
102
- content: [
103
- {
104
- type: "text",
105
- text: result,
106
- },
107
- ],
108
- };
109
114
  }
110
- else {
111
- throw new Error(`Unknown tool: ${name}`);
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;
112
122
  }
113
- }
114
- catch (error) {
115
- logMessage(mcpServer, "error", `Tool execution error: ${error instanceof Error ? error.message : String(error)}`, {
116
- tool: name,
117
- args: args,
118
- error: error instanceof Error ? error.stack : String(error)
119
- });
120
- throw error;
121
- }
122
- });
123
- // Logging level handler
124
- server.setRequestHandler(SetLevelRequestSchema, async (request) => {
125
- const { level } = request.params;
126
- logMessage(mcpServer, "info", `Setting log level to: ${level}`);
127
- currentLogLevel = 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
- });
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
+ }
186
188
  // Main function
187
189
  async function main() {
188
190
  // Check for HTTP transport mode
@@ -194,7 +196,7 @@ async function main() {
194
196
  process.exit(1);
195
197
  }
196
198
  console.log(`Starting HTTP transport on port ${port}`);
197
- const app = await createHttpServer(mcpServer);
199
+ const app = await createHttpServer(createMcpServer);
198
200
  const httpServer = app.listen(port, () => {
199
201
  console.log(`HTTP server listening on port ${port}`);
200
202
  console.log(`Health check: http://localhost:${port}/health`);
@@ -212,7 +214,8 @@ async function main() {
212
214
  process.on('SIGTERM', () => shutdown('SIGTERM'));
213
215
  }
214
216
  else {
215
- // Default STDIO transport
217
+ // Default STDIO transport — single session, single server
218
+ const mcpServer = createMcpServer();
216
219
  // Show helpful message when running in terminal
217
220
  if (process.stdin.isTTY) {
218
221
  console.error(`🔍 MCP SearXNG Server v${packageVersion} - Ready`);
@@ -228,7 +231,7 @@ async function main() {
228
231
  await mcpServer.connect(transport);
229
232
  // Log after connection is established
230
233
  logMessage(mcpServer, "info", `MCP SearXNG Server v${packageVersion} connected via STDIO`);
231
- logMessage(mcpServer, "info", `Log level: ${currentLogLevel}`);
234
+ logMessage(mcpServer, "info", `Log level: ${getCurrentLogLevel()}`);
232
235
  logMessage(mcpServer, "info", `Environment: ${process.env.NODE_ENV || 'development'}`);
233
236
  logMessage(mcpServer, "info", `SearXNG URL: ${process.env.SEARXNG_URL || 'not configured'}`);
234
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-searxng",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "mcpName": "io.github.ihor-sokoliuk/mcp-searxng",
5
5
  "description": "MCP server for SearXNG integration",
6
6
  "license": "MIT",