converse-mcp-server 1.0.1
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/.env.example +177 -0
- package/README.md +425 -0
- package/bin/converse.js +45 -0
- package/docs/API.md +897 -0
- package/docs/ARCHITECTURE.md +552 -0
- package/docs/EXAMPLES.md +736 -0
- package/package.json +101 -0
- package/src/config.js +521 -0
- package/src/continuationStore.js +340 -0
- package/src/index.js +216 -0
- package/src/providers/google.js +441 -0
- package/src/providers/index.js +87 -0
- package/src/providers/openai.js +348 -0
- package/src/providers/xai.js +305 -0
- package/src/router.js +497 -0
- package/src/systemPrompts.js +90 -0
- package/src/tools/chat.js +336 -0
- package/src/tools/consensus.js +478 -0
- package/src/tools/index.js +156 -0
- package/src/transport/httpTransport.js +548 -0
- package/src/utils/console.js +64 -0
- package/src/utils/contextProcessor.js +475 -0
- package/src/utils/errorHandler.js +555 -0
- package/src/utils/logger.js +450 -0
- package/src/utils/tokenLimiter.js +217 -0
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "converse-mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Simplified, functional Node.js implementation of MCP server with chat and consensus tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"converse": "bin/converse.js",
|
|
9
|
+
"converse-mcp-server": "bin/converse.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20.0.0"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"kill-server": "node scripts/kill-server.js",
|
|
16
|
+
"start": "npm run kill-server && node src/index.js",
|
|
17
|
+
"start:clean": "node src/index.js",
|
|
18
|
+
"start:port": "PORT=3001 npm run start:clean",
|
|
19
|
+
"dev": "npm run kill-server && NODE_ENV=development LOG_LEVEL=debug node --watch src/index.js",
|
|
20
|
+
"dev:clean": "NODE_ENV=development LOG_LEVEL=debug node --watch src/index.js",
|
|
21
|
+
"dev:port": "PORT=3001 npm run dev:clean",
|
|
22
|
+
"dev:quiet": "npm run kill-server && NODE_ENV=development LOG_LEVEL=info node --watch src/index.js",
|
|
23
|
+
"dev:verbose": "npm run kill-server && NODE_ENV=development LOG_LEVEL=trace node --watch src/index.js",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:unit": "vitest run --config vitest.unit.config.js",
|
|
27
|
+
"test:integration": "vitest run --config vitest.integration.config.js",
|
|
28
|
+
"test:mcp-client": "vitest run --config vitest.mcp-client.config.js",
|
|
29
|
+
"test:real-api": "vitest run --config vitest.real-api.config.js",
|
|
30
|
+
"test:ci": "vitest run --config vitest.ci.config.js",
|
|
31
|
+
"test:providers": "vitest run tests/providers",
|
|
32
|
+
"test:tools": "vitest run tests/tools",
|
|
33
|
+
"test:coverage": "vitest run --coverage",
|
|
34
|
+
"test:coverage:unit": "vitest run --config vitest.unit.config.js --coverage",
|
|
35
|
+
"test:coverage:integration": "vitest run --config vitest.integration.config.js --coverage",
|
|
36
|
+
"test:coverage:mcp-client": "vitest run --config vitest.mcp-client.config.js --coverage",
|
|
37
|
+
"test:ui": "vitest --ui",
|
|
38
|
+
"lint": "eslint src/ tests/",
|
|
39
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
40
|
+
"lint:watch": "nodemon --exec \"npm run lint\" --watch src --watch tests",
|
|
41
|
+
"format": "prettier --write src/ tests/ *.md *.json",
|
|
42
|
+
"format:check": "prettier --check src/ tests/ *.md *.json",
|
|
43
|
+
"typecheck": "node --check src/**/*.js",
|
|
44
|
+
"validate:simple": "npm run typecheck && npm run lint && npm run format:check && npm run test",
|
|
45
|
+
"clean": "rm -rf node_modules package-lock.json && npm install",
|
|
46
|
+
"debug": "NODE_ENV=development LOG_LEVEL=trace node --inspect src/index.js",
|
|
47
|
+
"debug:break": "NODE_ENV=development LOG_LEVEL=trace node --inspect-brk src/index.js",
|
|
48
|
+
"check-deps": "npm outdated",
|
|
49
|
+
"update-deps": "npm update",
|
|
50
|
+
"security-audit": "npm audit",
|
|
51
|
+
"build": "node scripts/build.js",
|
|
52
|
+
"build:fast": "node scripts/build.js --skip-tests",
|
|
53
|
+
"dev-server": "node dev-server.js",
|
|
54
|
+
"validate": "node scripts/validate.js",
|
|
55
|
+
"validate:fix": "node scripts/validate.js --fix",
|
|
56
|
+
"validate:fast": "node scripts/validate.js --skip-tests --skip-lint",
|
|
57
|
+
"precommit": "npm run validate"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"mcp",
|
|
61
|
+
"server",
|
|
62
|
+
"ai",
|
|
63
|
+
"chat",
|
|
64
|
+
"consensus",
|
|
65
|
+
"openai",
|
|
66
|
+
"google",
|
|
67
|
+
"gemini",
|
|
68
|
+
"grok"
|
|
69
|
+
],
|
|
70
|
+
"author": "Converse MCP Server",
|
|
71
|
+
"license": "MIT",
|
|
72
|
+
"homepage": "https://github.com/FallDownTheSystem/converse#readme",
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "git+https://github.com/FallDownTheSystem/converse.git"
|
|
76
|
+
},
|
|
77
|
+
"bugs": {
|
|
78
|
+
"url": "https://github.com/FallDownTheSystem/converse/issues"
|
|
79
|
+
},
|
|
80
|
+
"files": [
|
|
81
|
+
"src/",
|
|
82
|
+
"bin/",
|
|
83
|
+
"docs/",
|
|
84
|
+
"README.md",
|
|
85
|
+
".env.example"
|
|
86
|
+
],
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"@google/genai": "^1.11.0",
|
|
89
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
90
|
+
"cors": "^2.8.5",
|
|
91
|
+
"dotenv": "^16.4.5",
|
|
92
|
+
"express": "^5.1.0",
|
|
93
|
+
"openai": "^5.0.0"
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
97
|
+
"eslint": "^9.0.0",
|
|
98
|
+
"prettier": "^3.0.0",
|
|
99
|
+
"vitest": "^3.2.4"
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Management System
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive environment-based configuration system for the Converse MCP Server.
|
|
5
|
+
* Loads, validates, and manages all configuration from environment variables only.
|
|
6
|
+
* Follows functional architecture with explicit dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import dotenv from 'dotenv';
|
|
10
|
+
import { createLogger, configureLogger } from './utils/logger.js';
|
|
11
|
+
import { ConfigurationError } from './utils/errorHandler.js';
|
|
12
|
+
|
|
13
|
+
// Load environment variables from appropriate .env file
|
|
14
|
+
// Priority: .env.test (for test env) > .env (default)
|
|
15
|
+
if (process.env.NODE_ENV === 'test') {
|
|
16
|
+
// Load test environment first
|
|
17
|
+
dotenv.config({ path: '.env.test' });
|
|
18
|
+
// Fall back to .env for any missing variables
|
|
19
|
+
dotenv.config({ override: false });
|
|
20
|
+
} else {
|
|
21
|
+
// Load default .env file
|
|
22
|
+
dotenv.config();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Configure logger early
|
|
26
|
+
configureLogger({
|
|
27
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
28
|
+
isDevelopment: process.env.NODE_ENV === 'development'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const logger = createLogger('config');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Configuration schema defining all supported environment variables
|
|
35
|
+
*/
|
|
36
|
+
const CONFIG_SCHEMA = {
|
|
37
|
+
// Server configuration
|
|
38
|
+
server: {
|
|
39
|
+
PORT: { type: 'number', default: 3000, description: 'Server port' },
|
|
40
|
+
HOST: { type: 'string', default: 'localhost', description: 'Server host' },
|
|
41
|
+
NODE_ENV: { type: 'string', default: 'development', description: 'Environment mode' },
|
|
42
|
+
LOG_LEVEL: { type: 'string', default: 'info', description: 'Logging level' },
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Transport configuration
|
|
46
|
+
transport: {
|
|
47
|
+
MCP_TRANSPORT: { type: 'string', default: 'http', description: 'MCP transport type (http or stdio)' },
|
|
48
|
+
|
|
49
|
+
// HTTP server settings
|
|
50
|
+
HTTP_PORT: { type: 'number', default: 3000, description: 'HTTP server port' },
|
|
51
|
+
HTTP_HOST: { type: 'string', default: 'localhost', description: 'HTTP server host' },
|
|
52
|
+
HTTP_REQUEST_TIMEOUT: { type: 'number', default: 300000, description: 'HTTP request timeout in milliseconds (5 minutes)' },
|
|
53
|
+
HTTP_MAX_REQUEST_SIZE: { type: 'string', default: '10mb', description: 'Maximum HTTP request body size' },
|
|
54
|
+
|
|
55
|
+
// Session management
|
|
56
|
+
HTTP_SESSION_TIMEOUT: { type: 'number', default: 1800000, description: 'Session timeout in milliseconds (30 minutes)' },
|
|
57
|
+
HTTP_SESSION_CLEANUP_INTERVAL: { type: 'number', default: 300000, description: 'Session cleanup interval in milliseconds (5 minutes)' },
|
|
58
|
+
HTTP_MAX_CONCURRENT_SESSIONS: { type: 'number', default: 100, description: 'Maximum concurrent sessions' },
|
|
59
|
+
|
|
60
|
+
// CORS configuration
|
|
61
|
+
HTTP_ENABLE_CORS: { type: 'boolean', default: true, description: 'Enable CORS for HTTP transport' },
|
|
62
|
+
HTTP_CORS_ORIGINS: { type: 'string', default: '*', description: 'CORS allowed origins (comma-separated)' },
|
|
63
|
+
HTTP_CORS_METHODS: { type: 'string', default: 'GET,POST,DELETE,OPTIONS', description: 'CORS allowed methods' },
|
|
64
|
+
HTTP_CORS_HEADERS: { type: 'string', default: 'Content-Type,mcp-session-id,Authorization', description: 'CORS allowed headers' },
|
|
65
|
+
HTTP_CORS_CREDENTIALS: { type: 'boolean', default: false, description: 'CORS allow credentials' },
|
|
66
|
+
|
|
67
|
+
// Security settings
|
|
68
|
+
HTTP_DNS_REBINDING_PROTECTION: { type: 'boolean', default: false, description: 'Enable DNS rebinding protection' },
|
|
69
|
+
HTTP_ALLOWED_HOSTS: { type: 'string', default: '127.0.0.1,localhost', description: 'Allowed hosts for DNS rebinding protection (comma-separated)' },
|
|
70
|
+
HTTP_RATE_LIMIT_ENABLED: { type: 'boolean', default: false, description: 'Enable rate limiting' },
|
|
71
|
+
HTTP_RATE_LIMIT_WINDOW: { type: 'number', default: 900000, description: 'Rate limit window in milliseconds (15 minutes)' },
|
|
72
|
+
HTTP_RATE_LIMIT_MAX_REQUESTS: { type: 'number', default: 1000, description: 'Maximum requests per window' },
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// API Keys (at least one required)
|
|
76
|
+
apiKeys: {
|
|
77
|
+
OPENAI_API_KEY: { type: 'string', required: false, secret: true, description: 'OpenAI API key' },
|
|
78
|
+
XAI_API_KEY: { type: 'string', required: false, secret: true, description: 'XAI API key' },
|
|
79
|
+
GOOGLE_API_KEY: { type: 'string', required: false, secret: true, description: 'Google API key' },
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Provider-specific configuration
|
|
83
|
+
providers: {
|
|
84
|
+
GOOGLE_LOCATION: { type: 'string', default: 'us-central1', description: 'Google Cloud location' },
|
|
85
|
+
XAI_BASE_URL: { type: 'string', default: 'https://api.x.ai/v1', description: 'XAI API base URL' },
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// MCP configuration
|
|
89
|
+
mcp: {
|
|
90
|
+
MCP_SERVER_NAME: { type: 'string', default: 'converse-mcp-server', description: 'MCP server name' },
|
|
91
|
+
MCP_SERVER_VERSION: { type: 'string', default: '1.0.0', description: 'MCP server version' },
|
|
92
|
+
MAX_MCP_OUTPUT_TOKENS: { type: 'number', default: 25000, description: 'Maximum tokens in MCP tool responses' },
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ConfigurationError now imported from errorHandler
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validates and parses environment variable value according to schema
|
|
100
|
+
* @param {string} key - Environment variable key
|
|
101
|
+
* @param {string|undefined} value - Environment variable value
|
|
102
|
+
* @param {object} schema - Schema definition for the variable
|
|
103
|
+
* @returns {any} Parsed and validated value
|
|
104
|
+
*/
|
|
105
|
+
function validateEnvVar(key, value, schema) {
|
|
106
|
+
// Handle missing values
|
|
107
|
+
if (value === undefined || value === '') {
|
|
108
|
+
if (schema.required) {
|
|
109
|
+
throw new ConfigurationError(`Required environment variable ${key} is missing`);
|
|
110
|
+
}
|
|
111
|
+
return schema.default;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Type validation and conversion
|
|
115
|
+
switch (schema.type) {
|
|
116
|
+
case 'string':
|
|
117
|
+
return value;
|
|
118
|
+
case 'number':
|
|
119
|
+
const num = parseInt(value, 10);
|
|
120
|
+
if (isNaN(num)) {
|
|
121
|
+
throw new ConfigurationError(
|
|
122
|
+
`Environment variable ${key} must be a valid number, got: ${value}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return num;
|
|
126
|
+
case 'boolean':
|
|
127
|
+
const lower = value.toLowerCase();
|
|
128
|
+
if (!['true', 'false', '1', '0', 'yes', 'no'].includes(lower)) {
|
|
129
|
+
throw new ConfigurationError(
|
|
130
|
+
`Environment variable ${key} must be a boolean value, got: ${value}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return ['true', '1', 'yes'].includes(lower);
|
|
134
|
+
default:
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validates API key format and basic structure
|
|
141
|
+
* @param {string} provider - Provider name
|
|
142
|
+
* @param {string} apiKey - API key to validate
|
|
143
|
+
* @returns {boolean} True if API key appears valid
|
|
144
|
+
*/
|
|
145
|
+
function validateApiKeyFormat(provider, apiKey) {
|
|
146
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Basic format validation for each provider
|
|
151
|
+
switch (provider) {
|
|
152
|
+
case 'openai':
|
|
153
|
+
return apiKey.startsWith('sk-') && apiKey.length > 20;
|
|
154
|
+
case 'xai':
|
|
155
|
+
return apiKey.startsWith('xai-') && apiKey.length > 20;
|
|
156
|
+
case 'google':
|
|
157
|
+
return apiKey.length > 20; // Google keys vary in format
|
|
158
|
+
default:
|
|
159
|
+
return apiKey.length >= 10; // Basic minimum length check
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Loads and validates complete configuration from environment variables
|
|
165
|
+
* @returns {Promise<object>} Validated configuration object
|
|
166
|
+
* @throws {ConfigurationError} If configuration is invalid or incomplete
|
|
167
|
+
*/
|
|
168
|
+
export async function loadConfig() {
|
|
169
|
+
const configLogger = logger.operation('loadConfig');
|
|
170
|
+
configLogger.debug('Starting configuration loading');
|
|
171
|
+
|
|
172
|
+
const config = {
|
|
173
|
+
server: {},
|
|
174
|
+
transport: {},
|
|
175
|
+
apiKeys: {},
|
|
176
|
+
providers: {},
|
|
177
|
+
mcp: {},
|
|
178
|
+
environment: {
|
|
179
|
+
isDevelopment: false,
|
|
180
|
+
isProduction: false,
|
|
181
|
+
nodeEnv: '',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const errors = [];
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Load server configuration
|
|
189
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA.server)) {
|
|
190
|
+
try {
|
|
191
|
+
config.server[key.toLowerCase()] = validateEnvVar(key, process.env[key], schema);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
errors.push(error.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Load transport configuration
|
|
198
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA.transport)) {
|
|
199
|
+
try {
|
|
200
|
+
const value = validateEnvVar(key, process.env[key], schema);
|
|
201
|
+
|
|
202
|
+
if (key === 'MCP_TRANSPORT') {
|
|
203
|
+
config.transport.mcptransport = value;
|
|
204
|
+
} else if (key.startsWith('HTTP_')) {
|
|
205
|
+
// Convert HTTP_PORT -> port, HTTP_CORS_ORIGINS -> corsorigins, etc.
|
|
206
|
+
const configKey = key.replace('HTTP_', '').toLowerCase().replace(/_/g, '');
|
|
207
|
+
config.transport[configKey] = value;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
errors.push(error.message);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Load API keys
|
|
215
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA.apiKeys)) {
|
|
216
|
+
try {
|
|
217
|
+
const value = validateEnvVar(key, process.env[key], schema);
|
|
218
|
+
if (value) {
|
|
219
|
+
const providerName = key.replace('_API_KEY', '').toLowerCase();
|
|
220
|
+
config.apiKeys[providerName] = value;
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
errors.push(error.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Load provider configuration
|
|
228
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA.providers)) {
|
|
229
|
+
try {
|
|
230
|
+
const value = validateEnvVar(key, process.env[key], schema);
|
|
231
|
+
const configKey = key.toLowerCase().replace(/_/g, '');
|
|
232
|
+
config.providers[configKey] = value;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
errors.push(error.message);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Load MCP configuration
|
|
239
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA.mcp)) {
|
|
240
|
+
try {
|
|
241
|
+
const value = validateEnvVar(key, process.env[key], schema);
|
|
242
|
+
const configKey = key.replace('MCP_SERVER_', '').toLowerCase();
|
|
243
|
+
config.mcp[configKey] = value;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
errors.push(error.message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Set environment flags
|
|
250
|
+
const nodeEnv = config.server.node_env || 'development';
|
|
251
|
+
config.environment = {
|
|
252
|
+
isDevelopment: nodeEnv === 'development',
|
|
253
|
+
isProduction: nodeEnv === 'production',
|
|
254
|
+
nodeEnv,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Validate that at least one API key is present
|
|
258
|
+
const availableKeys = Object.keys(config.apiKeys);
|
|
259
|
+
if (availableKeys.length === 0) {
|
|
260
|
+
errors.push(
|
|
261
|
+
'At least one API key must be configured: OPENAI_API_KEY, XAI_API_KEY, or GOOGLE_API_KEY'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Validate API key formats
|
|
266
|
+
for (const [provider, apiKey] of Object.entries(config.apiKeys)) {
|
|
267
|
+
if (!validateApiKeyFormat(provider, apiKey)) {
|
|
268
|
+
errors.push(`Invalid API key format for ${provider.toUpperCase()}_API_KEY`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Throw accumulated errors
|
|
273
|
+
if (errors.length > 0) {
|
|
274
|
+
throw new ConfigurationError(
|
|
275
|
+
`Configuration validation failed with ${errors.length} error(s):\n${errors.map(e => ` - ${e}`).join('\n')}`,
|
|
276
|
+
{ errors }
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Log configuration summary (without secrets)
|
|
281
|
+
logConfigurationSummary(config);
|
|
282
|
+
configLogger.info('Configuration loaded successfully');
|
|
283
|
+
|
|
284
|
+
return config;
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
configLogger.error('Configuration loading failed', { error });
|
|
288
|
+
if (error instanceof ConfigurationError) {
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
throw new ConfigurationError(`Failed to load configuration: ${error.message}`, { originalError: error });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Gets HTTP transport configuration with proper structure
|
|
297
|
+
* @param {object} config - Main configuration object
|
|
298
|
+
* @returns {object} HTTP transport configuration
|
|
299
|
+
*/
|
|
300
|
+
export function getHttpTransportConfig(config) {
|
|
301
|
+
const transport = config.transport;
|
|
302
|
+
|
|
303
|
+
// Parse comma-separated values
|
|
304
|
+
const corsOrigins = transport.corsorigins === '*' ? '*' : transport.corsorigins?.split(',').map(o => o.trim()) || ['*'];
|
|
305
|
+
const corsMethods = transport.corsmethods?.split(',').map(m => m.trim()) || ['GET', 'POST', 'DELETE', 'OPTIONS'];
|
|
306
|
+
const corsHeaders = transport.corsheaders?.split(',').map(h => h.trim()) || ['Content-Type', 'mcp-session-id', 'Authorization'];
|
|
307
|
+
const allowedHosts = transport.allowedhosts?.split(',').map(h => h.trim()) || ['127.0.0.1', 'localhost'];
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
// Server settings
|
|
311
|
+
port: transport.port || 3000,
|
|
312
|
+
host: transport.host || 'localhost',
|
|
313
|
+
requestTimeout: transport.requesttimeout || 300000,
|
|
314
|
+
maxRequestSize: transport.maxrequestsize || '10mb',
|
|
315
|
+
|
|
316
|
+
// Session management
|
|
317
|
+
sessionTimeout: transport.sessiontimeout || 1800000,
|
|
318
|
+
sessionCleanupInterval: transport.sessioncleanupinterval || 300000,
|
|
319
|
+
maxConcurrentSessions: transport.maxconcurrentsessions || 100,
|
|
320
|
+
|
|
321
|
+
// CORS configuration
|
|
322
|
+
enableCors: transport.enablecors !== false,
|
|
323
|
+
corsOptions: {
|
|
324
|
+
origin: corsOrigins,
|
|
325
|
+
methods: corsMethods,
|
|
326
|
+
allowedHeaders: corsHeaders,
|
|
327
|
+
credentials: transport.corscredentials || false,
|
|
328
|
+
exposedHeaders: ['Mcp-Session-Id'],
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// Security settings
|
|
332
|
+
enableDnsRebindingProtection: transport.dnsrebindingprotection || false,
|
|
333
|
+
allowedHosts: allowedHosts,
|
|
334
|
+
rateLimitEnabled: transport.ratelimitenabled || false,
|
|
335
|
+
rateLimitWindow: transport.ratelimitwindow || 900000,
|
|
336
|
+
rateLimitMaxRequests: transport.ratelimitmaxrequests || 1000,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Gets configuration for a specific provider
|
|
342
|
+
* @param {object} config - Main configuration object
|
|
343
|
+
* @param {string} providerName - Name of the provider
|
|
344
|
+
* @returns {object} Provider-specific configuration
|
|
345
|
+
*/
|
|
346
|
+
export function getProviderConfig(config, providerName) {
|
|
347
|
+
const apiKey = config.apiKeys[providerName];
|
|
348
|
+
const providerConfig = {};
|
|
349
|
+
|
|
350
|
+
// Add provider-specific configuration
|
|
351
|
+
switch (providerName) {
|
|
352
|
+
case 'google':
|
|
353
|
+
providerConfig.location = config.providers.googlelocation;
|
|
354
|
+
break;
|
|
355
|
+
case 'xai':
|
|
356
|
+
providerConfig.baseUrl = config.providers.xaibaseurl;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
apiKey,
|
|
362
|
+
...providerConfig,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Checks if a provider is available (has valid API key)
|
|
368
|
+
* @param {object} config - Main configuration object
|
|
369
|
+
* @param {string} providerName - Name of the provider
|
|
370
|
+
* @returns {boolean} True if provider is available
|
|
371
|
+
*/
|
|
372
|
+
export function isProviderAvailable(config, providerName) {
|
|
373
|
+
const apiKey = config.apiKeys[providerName];
|
|
374
|
+
return apiKey && validateApiKeyFormat(providerName, apiKey);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Gets list of available providers
|
|
379
|
+
* @param {object} config - Main configuration object
|
|
380
|
+
* @returns {string[]} Array of available provider names
|
|
381
|
+
*/
|
|
382
|
+
export function getAvailableProviders(config) {
|
|
383
|
+
return Object.keys(config.apiKeys).filter(provider =>
|
|
384
|
+
isProviderAvailable(config, provider)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Validates runtime configuration consistency
|
|
390
|
+
* @param {object} config - Configuration object to validate
|
|
391
|
+
* @returns {Promise<boolean>} True if configuration is valid
|
|
392
|
+
* @throws {ConfigurationError} If configuration is invalid
|
|
393
|
+
*/
|
|
394
|
+
export async function validateRuntimeConfig(config) {
|
|
395
|
+
try {
|
|
396
|
+
// Validate server configuration
|
|
397
|
+
if (config.server.port < 1 || config.server.port > 65535) {
|
|
398
|
+
throw new ConfigurationError(`Invalid port number: ${config.server.port}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate environment
|
|
402
|
+
const validEnvs = ['development', 'production', 'test'];
|
|
403
|
+
if (!validEnvs.includes(config.environment.nodeEnv)) {
|
|
404
|
+
throw new ConfigurationError(
|
|
405
|
+
`Invalid NODE_ENV: ${config.environment.nodeEnv}. Must be one of: ${validEnvs.join(', ')}`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Validate log level
|
|
410
|
+
const validLogLevels = ['silent', 'error', 'warn', 'info', 'debug'];
|
|
411
|
+
if (!validLogLevels.includes(config.server.log_level)) {
|
|
412
|
+
throw new ConfigurationError(
|
|
413
|
+
`Invalid LOG_LEVEL: ${config.server.log_level}. Must be one of: ${validLogLevels.join(', ')}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Validate HTTP transport configuration
|
|
418
|
+
if (config.transport.mcptransport === 'http') {
|
|
419
|
+
const httpConfig = getHttpTransportConfig(config);
|
|
420
|
+
|
|
421
|
+
// Validate HTTP port
|
|
422
|
+
if (httpConfig.port < 1 || httpConfig.port > 65535) {
|
|
423
|
+
throw new ConfigurationError(`Invalid HTTP_PORT: ${httpConfig.port}. Must be between 1 and 65535`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Validate timeouts
|
|
427
|
+
if (httpConfig.requestTimeout < 1000) {
|
|
428
|
+
throw new ConfigurationError(`Invalid HTTP_REQUEST_TIMEOUT: ${httpConfig.requestTimeout}. Must be at least 1000ms`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (httpConfig.sessionTimeout < 60000) {
|
|
432
|
+
throw new ConfigurationError(`Invalid HTTP_SESSION_TIMEOUT: ${httpConfig.sessionTimeout}. Must be at least 60000ms (1 minute)`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (httpConfig.sessionCleanupInterval < 10000) {
|
|
436
|
+
throw new ConfigurationError(`Invalid HTTP_SESSION_CLEANUP_INTERVAL: ${httpConfig.sessionCleanupInterval}. Must be at least 10000ms (10 seconds)`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate max concurrent sessions
|
|
440
|
+
if (httpConfig.maxConcurrentSessions < 1 || httpConfig.maxConcurrentSessions > 10000) {
|
|
441
|
+
throw new ConfigurationError(`Invalid HTTP_MAX_CONCURRENT_SESSIONS: ${httpConfig.maxConcurrentSessions}. Must be between 1 and 10000`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Validate rate limiting
|
|
445
|
+
if (httpConfig.rateLimitEnabled) {
|
|
446
|
+
if (httpConfig.rateLimitWindow < 1000) {
|
|
447
|
+
throw new ConfigurationError(`Invalid HTTP_RATE_LIMIT_WINDOW: ${httpConfig.rateLimitWindow}. Must be at least 1000ms`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (httpConfig.rateLimitMaxRequests < 1) {
|
|
451
|
+
throw new ConfigurationError(`Invalid HTTP_RATE_LIMIT_MAX_REQUESTS: ${httpConfig.rateLimitMaxRequests}. Must be at least 1`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Production-specific validations
|
|
457
|
+
if (config.environment.isProduction) {
|
|
458
|
+
// Require at least 2 providers in production for redundancy
|
|
459
|
+
const availableProviders = getAvailableProviders(config);
|
|
460
|
+
if (availableProviders.length < 1) {
|
|
461
|
+
console.warn('Warning: Only one provider configured in production environment');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return true;
|
|
466
|
+
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (error instanceof ConfigurationError) {
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
throw new ConfigurationError(`Runtime validation failed: ${error.message}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Logs configuration summary (masking sensitive information)
|
|
477
|
+
* Only logs when NOT in MCP stdio mode to avoid interfering with JSON-RPC protocol
|
|
478
|
+
* @param {object} config - Configuration object
|
|
479
|
+
*/
|
|
480
|
+
function logConfigurationSummary(config) {
|
|
481
|
+
// Skip logging when running as MCP server to avoid interfering with JSON-RPC
|
|
482
|
+
if (process.stdin.isTTY === false || process.env.NODE_ENV === 'test') {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const availableProviders = getAvailableProviders(config);
|
|
487
|
+
|
|
488
|
+
// Only output configuration summary if not in silent mode
|
|
489
|
+
if (config.server.log_level !== 'silent') {
|
|
490
|
+
console.error('Configuration loaded successfully:');
|
|
491
|
+
console.error(` Environment: ${config.environment.nodeEnv}`);
|
|
492
|
+
console.error(` Port: ${config.server.port}`);
|
|
493
|
+
console.error(` Log Level: ${config.server.log_level}`);
|
|
494
|
+
console.error(` Available Providers: ${availableProviders.join(', ') || 'none'}`);
|
|
495
|
+
console.error(` MCP Server: ${config.mcp.name} v${config.mcp.version}`);
|
|
496
|
+
|
|
497
|
+
// Mask API keys in logs
|
|
498
|
+
const maskedKeys = Object.keys(config.apiKeys).map(key => {
|
|
499
|
+
const value = config.apiKeys[key];
|
|
500
|
+
return `${key.toUpperCase()}: ${value ? `${value.substring(0, 8)}...` : 'not configured'}`;
|
|
501
|
+
});
|
|
502
|
+
console.error(` API Keys: ${maskedKeys.join(', ')}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Creates MCP client configuration object
|
|
508
|
+
* @param {object} config - Main configuration object
|
|
509
|
+
* @returns {object} MCP client configuration
|
|
510
|
+
*/
|
|
511
|
+
export function getMcpClientConfig(config) {
|
|
512
|
+
return {
|
|
513
|
+
name: config.mcp.name,
|
|
514
|
+
version: config.mcp.version,
|
|
515
|
+
capabilities: {
|
|
516
|
+
tools: {},
|
|
517
|
+
},
|
|
518
|
+
environment: config.environment.nodeEnv,
|
|
519
|
+
providers: getAvailableProviders(config),
|
|
520
|
+
};
|
|
521
|
+
}
|