converse-mcp-server 1.0.2 → 1.1.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/README.md CHANGED
@@ -54,7 +54,7 @@ GOOGLE_API_KEY=your_google_api_key_here
54
54
  XAI_API_KEY=xai-your_xai_key_here
55
55
 
56
56
  # Optional: Server configuration
57
- PORT=3000
57
+ PORT=3157
58
58
  LOG_LEVEL=info
59
59
  MAX_MCP_OUTPUT_TOKENS=200000
60
60
 
@@ -172,14 +172,14 @@ LOG_LEVEL=debug npm run dev
172
172
 
173
173
  ```bash
174
174
  # Server management
175
- npm start # Start server (auto-kills existing server on port 3000)
175
+ npm start # Start server (auto-kills existing server on port 3157)
176
176
  npm run start:clean # Start server without killing existing processes
177
177
  npm run start:port # Start server on port 3001 (avoids port conflicts)
178
178
  npm run dev # Development with hot reload (auto-kills existing server)
179
179
  npm run dev:clean # Development without killing existing processes
180
180
  npm run dev:port # Development on port 3001 (avoids port conflicts)
181
181
  npm run dev:quiet # Development with minimal logging
182
- npm run kill-server # Kill any server running on port 3000
182
+ npm run kill-server # Kill any server running on port 3157
183
183
 
184
184
  # Testing
185
185
  npm test # Run all tests
@@ -198,15 +198,15 @@ npm run validate # Full validation (lint + test)
198
198
  npm run build # Build for production
199
199
  npm run debug # Start with debugger
200
200
  npm run check-deps # Check for outdated dependencies
201
- npm run kill-server # Kill any server running on port 3000
201
+ npm run kill-server # Kill any server running on port 3157
202
202
  ```
203
203
 
204
204
  ### 💡 Development Notes
205
205
 
206
- **Port Management**: The server runs on port 3000 by default for HTTP transport. If you encounter "EADDRINUSE" errors:
206
+ **Port Management**: The server runs on port 3157 by default for HTTP transport. If you encounter "EADDRINUSE" errors:
207
207
 
208
- 1. **Automatic cleanup**: `npm start` and `npm run dev` will automatically attempt to kill existing processes on port 3000
209
- 2. **Manual cleanup**: Run `npm run kill-server` to manually free up port 3000
208
+ 1. **Automatic cleanup**: `npm start` and `npm run dev` will automatically attempt to kill existing processes on port 3157
209
+ 2. **Manual cleanup**: Run `npm run kill-server` to manually free up port 3157
210
210
  3. **Clean start**: Use `:clean` variants (`npm run start:clean`, `npm run dev:clean`) to skip auto-cleanup
211
211
  4. **Persistent issues**: If port conflicts persist, manually kill Node.js processes or restart your terminal
212
212
 
@@ -223,7 +223,7 @@ npm start -- --transport=stdio
223
223
  ```
224
224
 
225
225
  **Transport Modes**:
226
- - **HTTP Transport** (default): `http://localhost:3000/mcp` - Better for development and debugging
226
+ - **HTTP Transport** (default): `http://localhost:3157/mcp` - Better for development and debugging
227
227
  - **Stdio Transport**: Use `--transport=stdio` or set `MCP_TRANSPORT=stdio` for traditional stdio communication
228
228
 
229
229
  ### Testing with Real APIs
@@ -263,7 +263,7 @@ node final-integration-test.js
263
263
  ```
264
264
 
265
265
  **Expected Results:**
266
- - Server starts without errors on port 3000
266
+ - Server starts without errors on port 3157
267
267
  - All unit tests pass
268
268
  - Real API tests connect successfully (if keys configured)
269
269
  - Integration tests achieve >70% success rate
@@ -363,7 +363,7 @@ converse/
363
363
 
364
364
  | Variable | Description | Default | Example |
365
365
  |----------|-------------|---------|---------|
366
- | `PORT` | Server port | `3000` | `3000` |
366
+ | `PORT` | Server port | `3157` | `3157` |
367
367
  | `LOG_LEVEL` | Logging level | `info` | `debug`, `info`, `error` |
368
368
  | `MAX_MCP_OUTPUT_TOKENS` | Token response limit | `25000` | `200000` |
369
369
  | `GOOGLE_LOCATION` | Google API region | `us-central1` | `us-central1` |
package/docs/API.md CHANGED
@@ -12,7 +12,7 @@ The Converse MCP Server provides two main tools through the Model Context Protoc
12
12
  The server supports two transport modes:
13
13
 
14
14
  ### HTTP Transport (Default)
15
- - **Endpoint**: `http://localhost:3000/mcp`
15
+ - **Endpoint**: `http://localhost:3157/mcp`
16
16
  - **Protocol**: HTTP streaming with JSON-RPC 2.0
17
17
  - **Usage**: Best for development, debugging, and web integrations
18
18
  - **Features**: Health endpoints, CORS support, session management
@@ -65,10 +65,10 @@ const server = new Server(
65
65
  { capabilities: { tools: {} } }
66
66
  );
67
67
 
68
- // HTTP transport on port 3000 (default)
68
+ // HTTP transport on port 3157 (default)
69
69
  const httpTransport = new StreamableHTTPServerTransport({
70
70
  host: 'localhost',
71
- port: 3000
71
+ port: 3157
72
72
  });
73
73
  ```
74
74
 
@@ -288,7 +288,7 @@ export const config = {
288
288
 
289
289
  // Server settings
290
290
  server: {
291
- port: parseInt(process.env.PORT) || 3000,
291
+ port: parseInt(process.env.PORT) || 3157,
292
292
  logLevel: process.env.LOG_LEVEL || 'info',
293
293
  maxOutputTokens: parseInt(process.env.MAX_MCP_OUTPUT_TOKENS) || 25000
294
294
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/config.js CHANGED
@@ -36,7 +36,7 @@ const logger = createLogger('config');
36
36
  const CONFIG_SCHEMA = {
37
37
  // Server configuration
38
38
  server: {
39
- PORT: { type: 'number', default: 3000, description: 'Server port' },
39
+ PORT: { type: 'number', default: 3157, description: 'Server port' },
40
40
  HOST: { type: 'string', default: 'localhost', description: 'Server host' },
41
41
  NODE_ENV: { type: 'string', default: 'development', description: 'Environment mode' },
42
42
  LOG_LEVEL: { type: 'string', default: 'info', description: 'Logging level' },
@@ -47,7 +47,7 @@ const CONFIG_SCHEMA = {
47
47
  MCP_TRANSPORT: { type: 'string', default: 'http', description: 'MCP transport type (http or stdio)' },
48
48
 
49
49
  // HTTP server settings
50
- HTTP_PORT: { type: 'number', default: 3000, description: 'HTTP server port' },
50
+ HTTP_PORT: { type: 'number', default: 3157, description: 'HTTP server port' },
51
51
  HTTP_HOST: { type: 'string', default: 'localhost', description: 'HTTP server host' },
52
52
  HTTP_REQUEST_TIMEOUT: { type: 'number', default: 300000, description: 'HTTP request timeout in milliseconds (5 minutes)' },
53
53
  HTTP_MAX_REQUEST_SIZE: { type: 'string', default: '10mb', description: 'Maximum HTTP request body size' },
@@ -219,16 +219,8 @@ export async function loadConfig() {
219
219
  }
220
220
  }
221
221
 
222
- // Load provider configuration
223
- for (const [key, schema] of Object.entries(CONFIG_SCHEMA.providers)) {
224
- try {
225
- const value = validateEnvVar(key, process.env[key], schema);
226
- const configKey = key.toLowerCase().replace(/_/g, '');
227
- config.providers[configKey] = value;
228
- } catch (error) {
229
- errors.push(error.message);
230
- }
231
- }
222
+ // Initialize providers object (no provider-specific config currently)
223
+ config.providers = {};
232
224
 
233
225
  // Load MCP configuration
234
226
  for (const [key, schema] of Object.entries(CONFIG_SCHEMA.mcp)) {
@@ -303,7 +295,7 @@ export function getHttpTransportConfig(config) {
303
295
 
304
296
  return {
305
297
  // Server settings
306
- port: transport.port || 3000,
298
+ port: transport.port || 3157,
307
299
  host: transport.host || 'localhost',
308
300
  requestTimeout: transport.requesttimeout || 300000,
309
301
  maxRequestSize: transport.maxrequestsize || '10mb',
package/src/index.js CHANGED
@@ -34,7 +34,7 @@ Options:
34
34
 
35
35
  Environment Variables:
36
36
  MCP_TRANSPORT Transport type (http or stdio)
37
- PORT HTTP server port (default: 3000)
37
+ PORT HTTP server port (default: 3157)
38
38
  HOST HTTP server host (default: localhost)
39
39
 
40
40
  Examples:
@@ -91,6 +91,20 @@ async function main() {
91
91
  process.exit(0);
92
92
  }
93
93
 
94
+ // Determine transport type first to configure logging appropriately
95
+ const transportType = getTransportType();
96
+
97
+ // Set environment variable early for stdio transport to suppress console output
98
+ if (transportType === 'stdio') {
99
+ process.env.MCP_TRANSPORT = 'stdio';
100
+ // Reconfigure logger with updated environment
101
+ const { configureLogger } = await import('./utils/logger.js');
102
+ configureLogger({
103
+ level: process.env.LOG_LEVEL || 'info',
104
+ isDevelopment: process.env.NODE_ENV === 'development'
105
+ });
106
+ }
107
+
94
108
  const serverTimer = startTimer('server-startup', 'server');
95
109
 
96
110
  try {
@@ -102,9 +116,7 @@ async function main() {
102
116
 
103
117
  // Get MCP client configuration
104
118
  const mcpConfig = getMcpClientConfig(config);
105
-
106
- // Determine transport type
107
- const transportType = getTransportType();
119
+
108
120
  logger.info('Using transport type', { data: { transport: transportType } });
109
121
 
110
122
  logger.debug('Creating MCP server instance', {
@@ -19,9 +19,10 @@ const SUPPORTED_MODELS = {
19
19
  supportsImages: true,
20
20
  supportsTemperature: true,
21
21
  supportsThinking: true,
22
+ supportsWebSearch: true,
22
23
  maxThinkingTokens: 24576,
23
24
  timeout: 300000,
24
- description: 'Gemini 2.0 Flash (1M context) - Latest fast model with experimental thinking, supports audio/video input',
25
+ description: 'Gemini 2.0 Flash (1M context) - Latest fast model with experimental thinking, supports audio/video input and grounding',
25
26
  aliases: ['flash-2.0', 'flash2']
26
27
  },
27
28
  'gemini-2.0-flash-lite': {
@@ -33,9 +34,10 @@ const SUPPORTED_MODELS = {
33
34
  supportsImages: false,
34
35
  supportsTemperature: true,
35
36
  supportsThinking: false,
37
+ supportsWebSearch: true,
36
38
  maxThinkingTokens: 0,
37
39
  timeout: 300000,
38
- description: 'Gemini 2.0 Flash Lite (1M context) - Lightweight fast model, text-only',
40
+ description: 'Gemini 2.0 Flash Lite (1M context) - Lightweight fast model, text-only with grounding',
39
41
  aliases: ['flashlite', 'flash-lite']
40
42
  },
41
43
  'gemini-2.5-flash': {
@@ -47,9 +49,10 @@ const SUPPORTED_MODELS = {
47
49
  supportsImages: true,
48
50
  supportsTemperature: true,
49
51
  supportsThinking: true,
52
+ supportsWebSearch: true,
50
53
  maxThinkingTokens: 24576,
51
54
  timeout: 300000,
52
- description: 'Ultra-fast (1M context) - Quick analysis, simple queries, rapid iterations',
55
+ description: 'Ultra-fast (1M context) - Quick analysis, simple queries, rapid iterations with grounding',
53
56
  aliases: ['flash', 'flash2.5', 'gemini-flash', 'gemini-flash-2.5']
54
57
  },
55
58
  'gemini-2.5-pro': {
@@ -58,6 +61,7 @@ const SUPPORTED_MODELS = {
58
61
  contextWindow: 1048576, // 1M tokens
59
62
  maxOutputTokens: 65536,
60
63
  supportsStreaming: true,
64
+ supportsWebSearch: true,
61
65
  supportsImages: true,
62
66
  supportsTemperature: true,
63
67
  supportsThinking: true,
@@ -160,18 +164,63 @@ function convertMessagesToGemini(messages) {
160
164
  // Google Gemini handles system prompts differently - they are typically prepended to the first user message
161
165
  systemPrompt = content;
162
166
  } else if (role === 'user') {
163
- // Combine system prompt with user message if present
164
- const userContent = systemPrompt ? `${systemPrompt}\n\n${content}` : content;
167
+ const parts = [];
168
+
169
+ // Handle complex content structure (array with text and images)
170
+ if (Array.isArray(content)) {
171
+ let textContent = '';
172
+
173
+ for (const item of content) {
174
+ if (item.type === 'text') {
175
+ textContent += item.text;
176
+ } else if (item.type === 'image' && item.source) {
177
+ // Convert Anthropic/Claude format to Google Gemini format
178
+ parts.push({
179
+ inlineData: {
180
+ mimeType: item.source.media_type,
181
+ data: item.source.data
182
+ }
183
+ });
184
+ debugLog(`[Google] Converting image: ${item.source.media_type}, data length: ${item.source.data.length}`);
185
+ }
186
+ }
187
+
188
+ // Combine system prompt with text content if present
189
+ const finalTextContent = systemPrompt ? `${systemPrompt}\n\n${textContent}` : textContent;
190
+ if (finalTextContent) {
191
+ parts.unshift({ text: finalTextContent });
192
+ }
193
+ } else {
194
+ // Simple string content
195
+ const userContent = systemPrompt ? `${systemPrompt}\n\n${content}` : content;
196
+ parts.push({ text: userContent });
197
+ }
198
+
165
199
  contents.push({
166
200
  role: 'user',
167
- parts: [{ text: userContent }]
201
+ parts: parts
168
202
  });
169
203
  systemPrompt = null; // Only use system prompt once
170
204
  } else if (role === 'assistant') {
171
- contents.push({
172
- role: 'model', // Google uses 'model' instead of 'assistant'
173
- parts: [{ text: content }]
174
- });
205
+ // Handle assistant messages
206
+ if (Array.isArray(content)) {
207
+ const parts = [];
208
+ for (const item of content) {
209
+ if (item.type === 'text') {
210
+ parts.push({ text: item.text });
211
+ }
212
+ // Assistant messages typically don't have images, but handle if needed
213
+ }
214
+ contents.push({
215
+ role: 'model', // Google uses 'model' instead of 'assistant'
216
+ parts: parts
217
+ });
218
+ } else {
219
+ contents.push({
220
+ role: 'model',
221
+ parts: [{ text: content }]
222
+ });
223
+ }
175
224
  }
176
225
  }
177
226
 
@@ -181,12 +230,12 @@ function convertMessagesToGemini(messages) {
181
230
  /**
182
231
  * Calculate thinking budget for models that support it
183
232
  */
184
- function calculateThinkingBudget(modelConfig, reasoningEffort) {
233
+ function calculateThinkingBudget(modelConfig, reasoning_effort) {
185
234
  if (!modelConfig.supportsThinking || !modelConfig.maxThinkingTokens) {
186
235
  return 0;
187
236
  }
188
237
 
189
- const budget = THINKING_BUDGETS[reasoningEffort] || THINKING_BUDGETS.medium;
238
+ const budget = THINKING_BUDGETS[reasoning_effort] || THINKING_BUDGETS.medium;
190
239
  return Math.floor(modelConfig.maxThinkingTokens * budget);
191
240
  }
192
241
 
@@ -281,7 +330,8 @@ export const googleProvider = {
281
330
  temperature = 0.7,
282
331
  maxTokens = null,
283
332
  stream: _unused_stream = false, // Acknowledged but not used yet
284
- reasoningEffort = 'medium',
333
+ reasoning_effort = 'medium',
334
+ use_websearch = false,
285
335
  config,
286
336
  ...otherOptions
287
337
  } = options;
@@ -321,15 +371,20 @@ export const googleProvider = {
321
371
  }
322
372
 
323
373
  // Add thinking configuration for models that support it
324
- if (modelConfig.supportsThinking && reasoningEffort) {
325
- const thinkingBudget = calculateThinkingBudget(modelConfig, reasoningEffort);
374
+ if (modelConfig.supportsThinking && reasoning_effort) {
375
+ const thinkingBudget = calculateThinkingBudget(modelConfig, reasoning_effort);
326
376
  if (thinkingBudget > 0) {
327
377
  generationConfig.thinkingConfig = { thinkingBudget };
328
378
  }
329
379
  }
330
380
 
381
+ // Add web search grounding if requested and model supports it
382
+ if (use_websearch && modelConfig.supportsWebSearch) {
383
+ generationConfig.tools = [{ googleSearch: {} }];
384
+ }
385
+
331
386
  try {
332
- debugLog(`[Google] Calling ${resolvedModel} with ${messages.length} messages`);
387
+ debugLog(`[Google] Calling ${resolvedModel} with ${messages.length} messages${use_websearch && modelConfig.supportsWebSearch ? ' (with grounding)' : ''}`);
333
388
 
334
389
  const startTime = Date.now();
335
390
 
@@ -371,8 +426,10 @@ export const googleProvider = {
371
426
  usage,
372
427
  response_time_ms: responseTime,
373
428
  finish_reason: finishReason,
374
- reasoning_effort: modelConfig.supportsThinking ? reasoningEffort : null,
375
- provider: 'google'
429
+ reasoning_effort: modelConfig.supportsThinking ? reasoning_effort : null,
430
+ provider: 'google',
431
+ web_search_used: use_websearch && modelConfig.supportsWebSearch,
432
+ grounding_metadata: response.groundingMetadata || null
376
433
  }
377
434
  };
378
435
 
@@ -18,6 +18,8 @@ const SUPPORTED_MODELS = {
18
18
  supportsStreaming: true,
19
19
  supportsImages: true,
20
20
  supportsTemperature: false,
21
+ supportsWebSearch: true,
22
+ supportsResponsesAPI: true,
21
23
  timeout: 300000, // 5 minutes
22
24
  description: 'Strong reasoning (200K context) - Logical problems, code generation, systematic analysis'
23
25
  },
@@ -29,6 +31,8 @@ const SUPPORTED_MODELS = {
29
31
  supportsStreaming: true,
30
32
  supportsImages: true,
31
33
  supportsTemperature: false,
34
+ supportsWebSearch: false, // o3-mini does not support web search
35
+ supportsResponsesAPI: true,
32
36
  timeout: 300000,
33
37
  description: 'Fast O3 variant (200K context) - Balanced performance/speed, moderate complexity',
34
38
  aliases: ['o3mini', 'o3 mini']
@@ -41,6 +45,8 @@ const SUPPORTED_MODELS = {
41
45
  supportsStreaming: true,
42
46
  supportsImages: true,
43
47
  supportsTemperature: false,
48
+ supportsWebSearch: true,
49
+ supportsResponsesAPI: true,
44
50
  timeout: 1800000, // 30 minutes
45
51
  description: 'Professional-grade reasoning (200K context) - EXTREMELY EXPENSIVE: Only for the most complex problems',
46
52
  aliases: ['o3-pro', 'o3pro', 'o3 pro']
@@ -52,7 +58,9 @@ const SUPPORTED_MODELS = {
52
58
  maxOutputTokens: 100000,
53
59
  supportsStreaming: true,
54
60
  supportsImages: true,
55
- supportsTemperature: true,
61
+ supportsTemperature: false,
62
+ supportsWebSearch: true,
63
+ supportsResponsesAPI: true,
56
64
  timeout: 180000, // 3 minutes
57
65
  description: 'Latest reasoning model (200K context) - Optimized for shorter contexts, rapid reasoning',
58
66
  aliases: ['o4mini', 'o4', 'o4 mini']
@@ -65,6 +73,8 @@ const SUPPORTED_MODELS = {
65
73
  supportsStreaming: true,
66
74
  supportsImages: true,
67
75
  supportsTemperature: true,
76
+ supportsWebSearch: true,
77
+ supportsResponsesAPI: true,
68
78
  timeout: 300000,
69
79
  description: 'GPT-4.1 (1M context) - Advanced reasoning model with large context window',
70
80
  aliases: ['gpt4.1', 'gpt-4.1', 'gpt 4.1']
@@ -77,6 +87,8 @@ const SUPPORTED_MODELS = {
77
87
  supportsStreaming: true,
78
88
  supportsImages: true,
79
89
  supportsTemperature: true,
90
+ supportsWebSearch: true,
91
+ supportsResponsesAPI: true,
80
92
  timeout: 180000,
81
93
  description: 'GPT-4o (128K context) - Multimodal flagship model with vision capabilities',
82
94
  aliases: ['gpt4o', 'gpt 4o', '4o']
@@ -89,6 +101,8 @@ const SUPPORTED_MODELS = {
89
101
  supportsStreaming: true,
90
102
  supportsImages: true,
91
103
  supportsTemperature: true,
104
+ supportsWebSearch: true,
105
+ supportsResponsesAPI: true,
92
106
  timeout: 120000,
93
107
  description: 'GPT-4o-mini (128K context) - Fast and efficient multimodal model',
94
108
  aliases: ['gpt4o-mini', 'gpt 4o mini', '4o mini', '4o-mini']
@@ -148,9 +162,9 @@ function validateApiKey(apiKey) {
148
162
  }
149
163
 
150
164
  /**
151
- * Convert messages to OpenAI format
165
+ * Convert messages to OpenAI format, handling both Responses API and Chat Completions API
152
166
  */
153
- function convertMessages(messages) {
167
+ function convertMessages(messages, useResponsesAPI = false) {
154
168
  if (!Array.isArray(messages)) {
155
169
  throw new OpenAIProviderError('Messages must be an array', 'INVALID_MESSAGES');
156
170
  }
@@ -170,6 +184,60 @@ function convertMessages(messages) {
170
184
  throw new OpenAIProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT');
171
185
  }
172
186
 
187
+ // Handle complex content structure (array with text and images)
188
+ if (Array.isArray(content)) {
189
+ debugLog(`[OpenAI] Processing complex content array with ${content.length} items for ${useResponsesAPI ? 'Responses API' : 'Chat Completions API'}`);
190
+ if (useResponsesAPI) {
191
+ // Convert to Responses API format
192
+ const convertedContent = [];
193
+
194
+ for (const item of content) {
195
+ if (item.type === 'text') {
196
+ convertedContent.push({
197
+ type: 'input_text',
198
+ text: item.text
199
+ });
200
+ } else if (item.type === 'image' && item.source) {
201
+ // Convert Anthropic/Claude format to OpenAI Responses API format
202
+ const imageUrl = `data:${item.source.media_type};base64,${item.source.data}`;
203
+ debugLog(`[OpenAI] Converting image for Responses API: ${item.source.media_type}, data length: ${item.source.data.length}`);
204
+ convertedContent.push({
205
+ type: 'input_image',
206
+ image_url: imageUrl
207
+ });
208
+ }
209
+ }
210
+
211
+ return { role, content: convertedContent };
212
+ } else {
213
+ // Convert to Chat Completions API format
214
+ const convertedContent = [];
215
+
216
+ for (const item of content) {
217
+ if (item.type === 'text') {
218
+ convertedContent.push({
219
+ type: 'text',
220
+ text: item.text
221
+ });
222
+ } else if (item.type === 'image' && item.source) {
223
+ // Convert Anthropic/Claude format to OpenAI Chat Completions format
224
+ const imageUrl = `data:${item.source.media_type};base64,${item.source.data}`;
225
+ debugLog(`[OpenAI] Converting image for Chat Completions API: ${item.source.media_type}, data length: ${item.source.data.length}`);
226
+ convertedContent.push({
227
+ type: 'image_url',
228
+ image_url: {
229
+ url: imageUrl,
230
+ detail: 'auto'
231
+ }
232
+ });
233
+ }
234
+ }
235
+
236
+ return { role, content: convertedContent };
237
+ }
238
+ }
239
+
240
+ // Simple string content
173
241
  return { role, content };
174
242
  });
175
243
  }
@@ -190,7 +258,8 @@ export const openaiProvider = {
190
258
  temperature = 0.7,
191
259
  maxTokens = null,
192
260
  stream = false,
193
- reasoningEffort = 'medium',
261
+ reasoning_effort = 'medium',
262
+ use_websearch = false,
194
263
  config,
195
264
  ...otherOptions
196
265
  } = options;
@@ -213,75 +282,129 @@ export const openaiProvider = {
213
282
  const resolvedModel = resolveModelName(model);
214
283
  const modelConfig = SUPPORTED_MODELS[resolvedModel] || {};
215
284
 
285
+ // Always use Responses API since all OpenAI models support it
286
+ // Only fallback to Chat Completions API if Responses API is explicitly not supported
287
+ const shouldUseResponsesAPI = modelConfig.supportsResponsesAPI !== false;
288
+
216
289
  // Convert and validate messages
217
- const openaiMessages = convertMessages(messages);
218
-
219
- // Build request payload (exclude reasoning_effort from otherOptions)
220
- const { reasoning_effort: _unused, ...cleanOptions } = otherOptions;
221
- const requestPayload = {
222
- model: resolvedModel,
223
- messages: openaiMessages,
224
- stream,
225
- ...cleanOptions
226
- };
227
-
228
- // Add temperature if model supports it
229
- if (modelConfig.supportsTemperature !== false && temperature !== undefined) {
230
- requestPayload.temperature = Math.max(0, Math.min(2, temperature));
290
+ const openaiMessages = convertMessages(messages, shouldUseResponsesAPI);
291
+
292
+ // Build request payload based on API type
293
+ let requestPayload;
294
+
295
+ if (shouldUseResponsesAPI) {
296
+ // Build Responses API payload
297
+ requestPayload = {
298
+ model: resolvedModel,
299
+ input: openaiMessages,
300
+ stream,
301
+ ...otherOptions
302
+ };
303
+
304
+ // Add web search tools only if requested and model supports it
305
+ if (use_websearch && modelConfig.supportsWebSearch) {
306
+ requestPayload.tools = [{ type: 'web_search' }];
307
+ }
308
+
309
+ // Add temperature if model supports it
310
+ if (modelConfig.supportsTemperature !== false && temperature !== undefined) {
311
+ requestPayload.temperature = Math.max(0, Math.min(2, temperature));
312
+ }
313
+
314
+ // Add reasoning effort for thinking models (o3 series only)
315
+ if (resolvedModel.startsWith('o3') && reasoning_effort) {
316
+ requestPayload.reasoning = { effort: reasoning_effort };
317
+ }
318
+ } else {
319
+ // Build Chat Completions API payload
320
+ const { reasoning_effort: _unused, ...cleanOptions } = otherOptions;
321
+ requestPayload = {
322
+ model: resolvedModel,
323
+ messages: openaiMessages,
324
+ stream,
325
+ ...cleanOptions
326
+ };
327
+
328
+ // Add temperature if model supports it
329
+ if (modelConfig.supportsTemperature !== false && temperature !== undefined) {
330
+ requestPayload.temperature = Math.max(0, Math.min(2, temperature));
331
+ }
332
+
333
+ // Add reasoning effort for thinking models (o3 series only)
334
+ if (resolvedModel.startsWith('o3') && reasoning_effort) {
335
+ requestPayload.reasoning_effort = reasoning_effort;
336
+ }
231
337
  }
232
-
233
- // Add max tokens if specified
338
+
339
+ // Add max tokens if specified (both APIs)
234
340
  if (maxTokens) {
235
- requestPayload.max_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 100000);
236
- }
237
-
238
- // Add reasoning effort for thinking models (o3 series only)
239
- if (resolvedModel.startsWith('o3') && reasoningEffort) {
240
- requestPayload.reasoning_effort = reasoningEffort;
341
+ if (shouldUseResponsesAPI) {
342
+ requestPayload.max_output_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 100000);
343
+ } else {
344
+ requestPayload.max_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 100000);
345
+ }
241
346
  }
242
- // Note: GPT-4o and other models don't support reasoning_effort parameter
243
- // Only O3 series models support this parameter
244
347
 
245
348
  try {
246
- debugLog(`[OpenAI] Calling ${resolvedModel} with ${openaiMessages.length} messages`);
349
+ const apiType = shouldUseResponsesAPI ? 'Responses API' : 'Chat Completions API';
350
+ debugLog(`[OpenAI] Calling ${resolvedModel} via ${apiType} with ${openaiMessages.length} messages${use_websearch && modelConfig.supportsWebSearch ? ' (with web search)' : ''}`);
247
351
 
248
352
  const startTime = Date.now();
249
353
 
250
- // Make the API call
251
- const response = await openai.chat.completions.create(requestPayload);
354
+ // Make the API call based on API type
355
+ let response;
356
+ if (shouldUseResponsesAPI) {
357
+ response = await openai.responses.create(requestPayload);
358
+ } else {
359
+ response = await openai.chat.completions.create(requestPayload);
360
+ }
252
361
 
253
362
  const responseTime = Date.now() - startTime;
254
363
  debugLog(`[OpenAI] Response received in ${responseTime}ms`);
255
364
 
256
- // Extract response data
257
- const choice = response.choices[0];
258
- if (!choice) {
259
- throw new OpenAIProviderError('No response choice received from OpenAI', 'NO_RESPONSE_CHOICE');
260
- }
261
-
262
- const content = choice.message?.content;
263
- if (!content) {
264
- throw new OpenAIProviderError('No content in response from OpenAI', 'NO_RESPONSE_CONTENT');
365
+ // Extract response data based on API type
366
+ let content, stopReason, usage;
367
+
368
+ if (shouldUseResponsesAPI) {
369
+ // Handle Responses API response format
370
+ if (!response.output_text) {
371
+ throw new OpenAIProviderError('No output_text in Responses API response', 'NO_RESPONSE_CONTENT');
372
+ }
373
+ content = response.output_text;
374
+ stopReason = response.status || 'stop';
375
+ usage = response.usage || {};
376
+ } else {
377
+ // Handle Chat Completions API response format
378
+ const choice = response.choices[0];
379
+ if (!choice) {
380
+ throw new OpenAIProviderError('No response choice received from OpenAI', 'NO_RESPONSE_CHOICE');
381
+ }
382
+
383
+ content = choice.message?.content;
384
+ if (!content) {
385
+ throw new OpenAIProviderError('No content in response from OpenAI', 'NO_RESPONSE_CONTENT');
386
+ }
387
+ stopReason = choice.finish_reason || 'stop';
388
+ usage = response.usage || {};
265
389
  }
266
390
 
267
- // Extract usage information
268
- const usage = response.usage || {};
269
-
270
391
  // Return unified response format
271
392
  return {
272
393
  content,
273
- stop_reason: choice.finish_reason || 'stop',
394
+ stop_reason: stopReason,
274
395
  rawResponse: response,
275
396
  metadata: {
276
397
  model: response.model || resolvedModel,
277
398
  usage: {
278
- input_tokens: usage.prompt_tokens || 0,
279
- output_tokens: usage.completion_tokens || 0,
399
+ input_tokens: usage.prompt_tokens || usage.input_tokens || 0,
400
+ output_tokens: usage.completion_tokens || usage.output_tokens || 0,
280
401
  total_tokens: usage.total_tokens || 0
281
402
  },
282
403
  response_time_ms: responseTime,
283
- finish_reason: choice.finish_reason,
284
- provider: 'openai'
404
+ finish_reason: stopReason,
405
+ provider: 'openai',
406
+ api_type: apiType,
407
+ web_search_used: use_websearch && modelConfig.supportsWebSearch
285
408
  }
286
409
  };
287
410
 
@@ -18,8 +18,9 @@ const SUPPORTED_MODELS = {
18
18
  supportsStreaming: true,
19
19
  supportsImages: true,
20
20
  supportsTemperature: true,
21
+ supportsWebSearch: true,
21
22
  timeout: 300000, // 5 minutes
22
- description: 'GROK-4 (256K context) - Latest advanced model from X.AI with image support',
23
+ description: 'GROK-4 (256K context) - Latest advanced model from X.AI with image support and live search',
23
24
  aliases: ['grok', 'grok4', 'grok-4', 'grok-4-latest', 'grok 4', 'grok 4 latest']
24
25
  },
25
26
  'grok-3': {
@@ -30,6 +31,7 @@ const SUPPORTED_MODELS = {
30
31
  supportsStreaming: true,
31
32
  supportsImages: false,
32
33
  supportsTemperature: true,
34
+ supportsWebSearch: false,
33
35
  timeout: 300000,
34
36
  description: 'GROK-3 (131K context) - Previous generation reasoning model from X.AI',
35
37
  aliases: ['grok3', 'grok 3']
@@ -42,6 +44,7 @@ const SUPPORTED_MODELS = {
42
44
  supportsStreaming: true,
43
45
  supportsImages: false,
44
46
  supportsTemperature: true,
47
+ supportsWebSearch: false,
45
48
  timeout: 300000,
46
49
  description: 'GROK-3 Fast (131K context) - Higher performance variant, faster processing but more expensive',
47
50
  aliases: ['grok3fast', 'grok3-fast', 'grok 3 fast']
@@ -123,6 +126,33 @@ function convertMessages(messages) {
123
126
  throw new XAIProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT');
124
127
  }
125
128
 
129
+ // Handle complex content structure (array with text and images)
130
+ if (Array.isArray(content)) {
131
+ const convertedContent = [];
132
+
133
+ for (const item of content) {
134
+ if (item.type === 'text') {
135
+ convertedContent.push({
136
+ type: 'text',
137
+ text: item.text
138
+ });
139
+ } else if (item.type === 'image' && item.source) {
140
+ // Convert Anthropic/Claude format to OpenAI format for XAI
141
+ convertedContent.push({
142
+ type: 'image_url',
143
+ image_url: {
144
+ url: `data:${item.source.media_type};base64,${item.source.data}`,
145
+ detail: 'auto'
146
+ }
147
+ });
148
+ debugLog(`[XAI] Converting image: ${item.source.media_type}, data length: ${item.source.data.length}`);
149
+ }
150
+ }
151
+
152
+ return { role, content: convertedContent };
153
+ }
154
+
155
+ // Simple string content
126
156
  return { role, content };
127
157
  });
128
158
  }
@@ -143,7 +173,8 @@ export const xaiProvider = {
143
173
  temperature = 0.7,
144
174
  maxTokens = null,
145
175
  stream = false,
146
- reasoningEffort = 'medium',
176
+ reasoning_effort = 'medium',
177
+ use_websearch = false,
147
178
  config,
148
179
  ...otherOptions
149
180
  } = options;
@@ -174,7 +205,7 @@ export const xaiProvider = {
174
205
  const xaiMessages = convertMessages(messages);
175
206
 
176
207
  // Filter out unsupported parameters for XAI/Grok models
177
- const { reasoning_effort, reasoningEffort: reasoningEffortAlias, ...supportedOptions } = otherOptions;
208
+ const { reasoning_effort: _unused_reasoning_effort, ...supportedOptions } = otherOptions;
178
209
 
179
210
  // Build request payload
180
211
  const requestPayload = {
@@ -194,11 +225,18 @@ export const xaiProvider = {
194
225
  requestPayload.max_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 256000);
195
226
  }
196
227
 
228
+ // Add web search parameters if requested and model supports it
229
+ if (use_websearch && modelConfig.supportsWebSearch) {
230
+ requestPayload.search_parameters = {
231
+ mode: 'auto' // Let the model decide when to use web search
232
+ };
233
+ }
234
+
197
235
  // Note: XAI/Grok models don't currently support reasoning_effort parameter
198
236
  // We silently ignore it for API consistency (no need to log warnings in tests)
199
237
 
200
238
  try {
201
- debugLog(`[XAI] Calling ${resolvedModel} with ${xaiMessages.length} messages`);
239
+ debugLog(`[XAI] Calling ${resolvedModel} with ${xaiMessages.length} messages${use_websearch && modelConfig.supportsWebSearch ? ' (with live search)' : ''}`);
202
240
 
203
241
  const startTime = Date.now();
204
242
 
@@ -236,7 +274,8 @@ export const xaiProvider = {
236
274
  },
237
275
  response_time_ms: responseTime,
238
276
  finish_reason: choice.finish_reason,
239
- provider: 'xai'
277
+ provider: 'xai',
278
+ web_search_used: use_websearch && modelConfig.supportsWebSearch
240
279
  }
241
280
  };
242
281
 
package/src/tools/chat.js CHANGED
@@ -12,6 +12,7 @@ import { debugLog, debugError } from '../utils/console.js';
12
12
  import { createLogger } from '../utils/logger.js';
13
13
  import { CHAT_PROMPT } from '../systemPrompts.js';
14
14
  import { applyTokenLimit, getTokenLimit } from '../utils/tokenLimiter.js';
15
+ import { validateAllPaths } from '../utils/fileValidator.js';
15
16
 
16
17
  const logger = createLogger('chat');
17
18
 
@@ -65,6 +66,15 @@ export async function chatTool(args, dependencies) {
65
66
  continuationId = generateContinuationId();
66
67
  }
67
68
 
69
+ // Validate file paths before processing
70
+ if (files.length > 0 || images.length > 0) {
71
+ const validation = await validateAllPaths({ files, images });
72
+ if (!validation.valid) {
73
+ logger.error('File validation failed', { errors: validation.errors });
74
+ return validation.errorResponse;
75
+ }
76
+ }
77
+
68
78
  // Process context (files, images, web search)
69
79
  let contextMessage = null;
70
80
  if (files.length > 0 || images.length > 0 || use_websearch) {
@@ -77,9 +87,10 @@ export async function chatTool(args, dependencies) {
77
87
 
78
88
  const contextResult = await contextProcessor.processUnifiedContext(contextRequest);
79
89
 
80
- // Create context message from files
81
- if (contextResult.files.length > 0) {
82
- contextMessage = createFileContext(contextResult.files, {
90
+ // Create context message from files and images
91
+ const allProcessedFiles = [...contextResult.files, ...contextResult.images];
92
+ if (allProcessedFiles.length > 0) {
93
+ contextMessage = createFileContext(allProcessedFiles, {
83
94
  includeMetadata: true,
84
95
  includeErrors: true
85
96
  });
@@ -111,16 +122,22 @@ export async function chatTool(args, dependencies) {
111
122
  // Add conversation history
112
123
  messages.push(...conversationHistory);
113
124
 
114
- // Add context message if available
115
- if (contextMessage) {
116
- messages.push(contextMessage);
125
+ // Add user prompt with context
126
+ const userMessage = {
127
+ role: 'user',
128
+ content: prompt // default to simple string content
129
+ };
130
+
131
+ // If we have context (files/images), create complex content array
132
+ if (contextMessage && contextMessage.content) {
133
+ // Create complex content array
134
+ userMessage.content = [
135
+ ...contextMessage.content, // Include all file/image parts
136
+ { type: 'text', text: prompt } // Add the user prompt as text
137
+ ];
117
138
  }
118
139
 
119
- // Add user prompt
120
- messages.push({
121
- role: 'user',
122
- content: prompt
123
- });
140
+ messages.push(userMessage);
124
141
 
125
142
  // Select provider
126
143
  let selectedProvider;
@@ -160,6 +177,7 @@ export async function chatTool(args, dependencies) {
160
177
  model: resolvedModel,
161
178
  temperature,
162
179
  reasoning_effort,
180
+ use_websearch: use_websearch,
163
181
  config
164
182
  };
165
183
 
@@ -12,6 +12,7 @@ import { debugLog, debugError } from '../utils/console.js';
12
12
  import { createLogger } from '../utils/logger.js';
13
13
  import { CONSENSUS_PROMPT } from '../systemPrompts.js';
14
14
  import { applyTokenLimit, getTokenLimit } from '../utils/tokenLimiter.js';
15
+ import { validateAllPaths } from '../utils/fileValidator.js';
15
16
 
16
17
  const logger = createLogger('consensus');
17
18
 
@@ -44,7 +45,8 @@ export async function consensusTool(args, dependencies) {
44
45
  enable_cross_feedback = true,
45
46
  cross_feedback_prompt,
46
47
  temperature = 0.2,
47
- reasoning_effort = 'medium'
48
+ reasoning_effort = 'medium',
49
+ use_websearch = false
48
50
  } = args;
49
51
 
50
52
  let conversationHistory = [];
@@ -70,6 +72,18 @@ export async function consensusTool(args, dependencies) {
70
72
  continuationId = generateContinuationId();
71
73
  }
72
74
 
75
+ // Validate file paths before processing
76
+ if (relevant_files.length > 0 || images.length > 0) {
77
+ const validation = await validateAllPaths({
78
+ files: relevant_files,
79
+ images: images
80
+ });
81
+ if (!validation.valid) {
82
+ logger.error('File validation failed', { errors: validation.errors });
83
+ return validation.errorResponse;
84
+ }
85
+ }
86
+
73
87
  // Process context (files and images)
74
88
  let contextMessage = null;
75
89
  if (relevant_files.length > 0 || images.length > 0) {
@@ -81,9 +95,10 @@ export async function consensusTool(args, dependencies) {
81
95
 
82
96
  const contextResult = await contextProcessor.processUnifiedContext(contextRequest);
83
97
 
84
- // Create context message from files
85
- if (contextResult.files.length > 0) {
86
- contextMessage = createFileContext(contextResult.files, {
98
+ // Create context message from files and images
99
+ const allProcessedFiles = [...contextResult.files, ...contextResult.images];
100
+ if (allProcessedFiles.length > 0) {
101
+ contextMessage = createFileContext(allProcessedFiles, {
87
102
  includeMetadata: true,
88
103
  includeErrors: true
89
104
  });
@@ -107,16 +122,22 @@ export async function consensusTool(args, dependencies) {
107
122
  // Add conversation history
108
123
  messages.push(...conversationHistory);
109
124
 
110
- // Add context message if available
111
- if (contextMessage) {
112
- messages.push(contextMessage);
125
+ // Add user prompt with context
126
+ const userMessage = {
127
+ role: 'user',
128
+ content: prompt // default to simple string content
129
+ };
130
+
131
+ // If we have context (files/images), create complex content array
132
+ if (contextMessage && contextMessage.content) {
133
+ // Create complex content array
134
+ userMessage.content = [
135
+ ...contextMessage.content, // Include all file/image parts
136
+ { type: 'text', text: prompt } // Add the user prompt as text
137
+ ];
113
138
  }
114
139
 
115
- // Add user prompt
116
- messages.push({
117
- role: 'user',
118
- content: prompt
119
- });
140
+ messages.push(userMessage);
120
141
 
121
142
  // Resolve model specifications to provider calls
122
143
  const providerCalls = [];
@@ -165,6 +186,7 @@ export async function consensusTool(args, dependencies) {
165
186
  model: resolvedModelName, // Use resolved model name for API call
166
187
  temperature,
167
188
  reasoning_effort,
189
+ use_websearch: use_websearch,
168
190
  config,
169
191
  ...modelSpec // Allow model-specific overrides
170
192
  }
@@ -476,6 +498,11 @@ consensusTool.inputSchema = {
476
498
  description: 'Reasoning depth for thinking models. Examples: "medium" (balanced - default), "high" (complex analysis), "max" (thorough evaluation). Default: "medium"',
477
499
  default: 'medium'
478
500
  },
501
+ use_websearch: {
502
+ type: 'boolean',
503
+ description: 'Enable web search for current information and best practices. Only works with models that support web search (OpenAI, XAI, Google). Example: true for recent developments, false for analysis. Default: false',
504
+ default: false
505
+ },
479
506
  },
480
507
  required: ['prompt', 'models'],
481
508
  };
@@ -22,7 +22,7 @@ export class HTTPTransportServer {
22
22
  constructor(config = {}) {
23
23
  this.config = {
24
24
  // Server settings
25
- port: config.port || 3000,
25
+ port: config.port || 3157,
26
26
  host: config.host || 'localhost',
27
27
  requestTimeout: config.requestTimeout || 300000,
28
28
  maxRequestSize: config.maxRequestSize || '10mb',
@@ -0,0 +1,90 @@
1
+ /**
2
+ * File Validator Utility
3
+ *
4
+ * Validates that file paths exist before processing them.
5
+ * Returns early with clear error messages if any files are not found.
6
+ */
7
+
8
+ import { access, constants } from 'fs/promises';
9
+ import { resolve, isAbsolute } from 'path';
10
+ import { createToolError } from '../tools/index.js';
11
+
12
+ /**
13
+ * Validate that all provided file paths exist
14
+ * @param {string[]} filePaths - Array of file paths to validate
15
+ * @param {string} fileType - Type of files being validated (e.g., 'file', 'image')
16
+ * @returns {Promise<{valid: boolean, missingPaths: string[], error?: object}>}
17
+ */
18
+ export async function validateFilePaths(filePaths, fileType = 'file') {
19
+ if (!Array.isArray(filePaths) || filePaths.length === 0) {
20
+ return { valid: true, missingPaths: [] };
21
+ }
22
+
23
+ const missingPaths = [];
24
+
25
+ for (const filePath of filePaths) {
26
+ if (!filePath || typeof filePath !== 'string') {
27
+ missingPaths.push(`Invalid path: ${filePath}`);
28
+ continue;
29
+ }
30
+
31
+ // Convert to absolute path if needed
32
+ const absolutePath = isAbsolute(filePath)
33
+ ? filePath
34
+ : resolve(process.cwd(), filePath);
35
+
36
+ try {
37
+ // Check if file exists and is readable
38
+ await access(absolutePath, constants.R_OK);
39
+ } catch (error) {
40
+ // Keep the original path in the error message for clarity
41
+ missingPaths.push(filePath);
42
+ }
43
+ }
44
+
45
+ if (missingPaths.length > 0) {
46
+ const errorMessage = `The following ${fileType}${missingPaths.length > 1 ? 's' : ''} could not be found: ${missingPaths.join(', ')}`;
47
+ return {
48
+ valid: false,
49
+ missingPaths,
50
+ error: createToolError(errorMessage)
51
+ };
52
+ }
53
+
54
+ return { valid: true, missingPaths: [] };
55
+ }
56
+
57
+ /**
58
+ * Validate both files and images together
59
+ * @param {object} paths - Object containing files and images arrays
60
+ * @returns {Promise<{valid: boolean, errors: string[], errorResponse?: object}>}
61
+ */
62
+ export async function validateAllPaths({ files = [], images = [] }) {
63
+ const errors = [];
64
+
65
+ // Validate regular files
66
+ if (files.length > 0) {
67
+ const fileValidation = await validateFilePaths(files, 'file');
68
+ if (!fileValidation.valid) {
69
+ errors.push(`Files not found: ${fileValidation.missingPaths.join(', ')}`);
70
+ }
71
+ }
72
+
73
+ // Validate image files
74
+ if (images.length > 0) {
75
+ const imageValidation = await validateFilePaths(images, 'image');
76
+ if (!imageValidation.valid) {
77
+ errors.push(`Images not found: ${imageValidation.missingPaths.join(', ')}`);
78
+ }
79
+ }
80
+
81
+ if (errors.length > 0) {
82
+ return {
83
+ valid: false,
84
+ errors,
85
+ errorResponse: createToolError(errors.join('. '))
86
+ };
87
+ }
88
+
89
+ return { valid: true, errors: [] };
90
+ }