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.
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Continuation Store - State Management
3
+ *
4
+ * Manages conversation history and state for persistent conversations.
5
+ * Pluggable implementation that can be swapped for different storage backends.
6
+ * Provides a consistent interface (get/set/delete) for state management.
7
+ */
8
+
9
+ import { randomUUID } from 'crypto';
10
+ import { debugLog, debugError } from './utils/console.js';
11
+
12
+ /**
13
+ * Storage backend interface that all continuation stores must implement
14
+ * This ensures pluggable backend replacement without changing the API
15
+ */
16
+ export class ContinuationStoreInterface {
17
+ /**
18
+ * Store conversation state
19
+ * @param {string} continuationId - Unique continuation identifier
20
+ * @param {object} state - Conversation state
21
+ * @returns {Promise<void>}
22
+ */
23
+ async set(continuationId, state) {
24
+ throw new Error('set() method must be implemented by storage backend');
25
+ }
26
+
27
+ /**
28
+ * Retrieve conversation state
29
+ * @param {string} continuationId - Unique continuation identifier
30
+ * @returns {Promise<object|null>} State or null if not found
31
+ */
32
+ async get(continuationId) {
33
+ throw new Error('get() method must be implemented by storage backend');
34
+ }
35
+
36
+ /**
37
+ * Delete conversation state
38
+ * @param {string} continuationId - Unique continuation identifier
39
+ * @returns {Promise<boolean>} True if deleted, false if not found
40
+ */
41
+ async delete(continuationId) {
42
+ throw new Error('delete() method must be implemented by storage backend');
43
+ }
44
+
45
+ /**
46
+ * Check if continuation exists
47
+ * @param {string} continuationId - Unique continuation identifier
48
+ * @returns {Promise<boolean>} True if exists
49
+ */
50
+ async exists(continuationId) {
51
+ const state = await this.get(continuationId);
52
+ return state !== null;
53
+ }
54
+
55
+ /**
56
+ * Get storage statistics
57
+ * @returns {Promise<object>} Backend-specific statistics
58
+ */
59
+ async getStats() {
60
+ throw new Error('getStats() method must be implemented by storage backend');
61
+ }
62
+
63
+ /**
64
+ * Clean up old data
65
+ * @param {number} maxAgeMs - Maximum age in milliseconds
66
+ * @returns {Promise<number>} Number of items cleaned up
67
+ */
68
+ async cleanup(maxAgeMs) {
69
+ throw new Error('cleanup() method must be implemented by storage backend');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Custom error class for continuation store operations
75
+ */
76
+ export class ContinuationStoreError extends Error {
77
+ constructor(message, code = 'CONTINUATION_ERROR') {
78
+ super(message);
79
+ this.name = 'ContinuationStoreError';
80
+ this.code = code;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * In-memory continuation store implementation
86
+ * Implements the ContinuationStoreInterface for pluggable backend replacement
87
+ */
88
+ class MemoryContinuationStore extends ContinuationStoreInterface {
89
+ constructor() {
90
+ super(); // Call parent constructor
91
+ this.conversations = new Map();
92
+ this.maxConversations = 1000; // Prevent memory leaks
93
+ this.maxMessagesPerConversation = 100;
94
+ }
95
+
96
+ /**
97
+ * Store conversation state
98
+ * @param {string} continuationId - Unique continuation identifier
99
+ * @param {object} state - Conversation state to store
100
+ * @returns {Promise<void>}
101
+ * @throws {ContinuationStoreError} If storage fails
102
+ */
103
+ async set(continuationId, state) {
104
+ try {
105
+ // Validate continuation ID
106
+ if (!continuationId || typeof continuationId !== 'string') {
107
+ throw new ContinuationStoreError(
108
+ 'Invalid continuation ID: must be a non-empty string',
109
+ 'INVALID_CONTINUATION_ID'
110
+ );
111
+ }
112
+
113
+ // Validate state object
114
+ if (!state || typeof state !== 'object') {
115
+ throw new ContinuationStoreError(
116
+ 'Invalid state: must be an object',
117
+ 'INVALID_STATE'
118
+ );
119
+ }
120
+ // Cleanup old conversations if we hit the limit
121
+ if (this.conversations.size >= this.maxConversations) {
122
+ const oldestKey = this.conversations.keys().next().value;
123
+ this.conversations.delete(oldestKey);
124
+ }
125
+
126
+ // Limit messages per conversation to prevent memory issues
127
+ const sanitizedState = { ...state };
128
+ if (sanitizedState.messages && sanitizedState.messages.length > this.maxMessagesPerConversation) {
129
+ sanitizedState.messages = sanitizedState.messages.slice(-this.maxMessagesPerConversation);
130
+ }
131
+
132
+ // Store with metadata
133
+ this.conversations.set(continuationId, {
134
+ ...sanitizedState,
135
+ lastAccessed: Date.now(),
136
+ createdAt: this.conversations.has(continuationId)
137
+ ? this.conversations.get(continuationId).createdAt
138
+ : Date.now(),
139
+ });
140
+
141
+ } catch (error) {
142
+ if (error instanceof ContinuationStoreError) {
143
+ throw error;
144
+ }
145
+ throw new ContinuationStoreError(
146
+ `Failed to store continuation: ${error.message}`,
147
+ 'STORAGE_ERROR'
148
+ );
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Retrieve conversation state
154
+ * @param {string} continuationId - Unique continuation identifier
155
+ * @returns {Promise<object|null>} Conversation state or null if not found
156
+ * @throws {ContinuationStoreError} If retrieval fails
157
+ */
158
+ async get(continuationId) {
159
+ try {
160
+ // Validate continuation ID
161
+ if (!continuationId || typeof continuationId !== 'string') {
162
+ throw new ContinuationStoreError(
163
+ 'Invalid continuation ID: must be a non-empty string',
164
+ 'INVALID_CONTINUATION_ID'
165
+ );
166
+ }
167
+
168
+ const state = this.conversations.get(continuationId);
169
+ if (state) {
170
+ // Update last accessed time
171
+ state.lastAccessed = Date.now();
172
+ // Return copy without internal metadata
173
+ const { createdAt, lastAccessed, ...cleanState } = state;
174
+ return {
175
+ ...cleanState,
176
+ _metadata: { createdAt, lastAccessed }
177
+ };
178
+ }
179
+ return null;
180
+
181
+ } catch (error) {
182
+ if (error instanceof ContinuationStoreError) {
183
+ throw error;
184
+ }
185
+ throw new ContinuationStoreError(
186
+ `Failed to retrieve continuation: ${error.message}`,
187
+ 'RETRIEVAL_ERROR'
188
+ );
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Delete conversation state
194
+ * @param {string} continuationId - Unique continuation identifier
195
+ * @returns {Promise<boolean>} True if deleted, false if not found
196
+ * @throws {ContinuationStoreError} If deletion fails
197
+ */
198
+ async delete(continuationId) {
199
+ try {
200
+ // Validate continuation ID
201
+ if (!continuationId || typeof continuationId !== 'string') {
202
+ throw new ContinuationStoreError(
203
+ 'Invalid continuation ID: must be a non-empty string',
204
+ 'INVALID_CONTINUATION_ID'
205
+ );
206
+ }
207
+
208
+ const existed = this.conversations.has(continuationId);
209
+ this.conversations.delete(continuationId);
210
+ return existed;
211
+
212
+ } catch (error) {
213
+ if (error instanceof ContinuationStoreError) {
214
+ throw error;
215
+ }
216
+ throw new ContinuationStoreError(
217
+ `Failed to delete continuation: ${error.message}`,
218
+ 'DELETION_ERROR'
219
+ );
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Get storage statistics
225
+ * @returns {Promise<object>} Store statistics
226
+ */
227
+ async getStats() {
228
+ return {
229
+ backend: 'memory',
230
+ totalConversations: this.conversations.size,
231
+ maxConversations: this.maxConversations,
232
+ maxMessagesPerConversation: this.maxMessagesPerConversation,
233
+ memoryUsage: process.memoryUsage(),
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Clean up old conversations
239
+ * @param {number} maxAgeMs - Maximum age in milliseconds (default: 24 hours)
240
+ * @returns {Promise<number>} Number of conversations cleaned up
241
+ */
242
+ async cleanup(maxAgeMs = 24 * 60 * 60 * 1000) {
243
+ const now = Date.now();
244
+ let cleanedCount = 0;
245
+
246
+ // Special case: if maxAgeMs is 0, clean up all conversations
247
+ if (maxAgeMs === 0) {
248
+ cleanedCount = this.conversations.size;
249
+ this.conversations.clear();
250
+ return cleanedCount;
251
+ }
252
+
253
+ for (const [id, state] of this.conversations.entries()) {
254
+ if (now - state.lastAccessed > maxAgeMs) {
255
+ this.conversations.delete(id);
256
+ cleanedCount++;
257
+ }
258
+ }
259
+
260
+ return cleanedCount;
261
+ }
262
+ }
263
+
264
+ // Singleton instance - can be replaced for different backends
265
+ let continuationStore = null;
266
+
267
+ /**
268
+ * Get the continuation store instance
269
+ * @returns {ContinuationStoreInterface} Continuation store instance
270
+ */
271
+ export function getContinuationStore() {
272
+ if (!continuationStore) {
273
+ continuationStore = new MemoryContinuationStore();
274
+
275
+ // Set up periodic cleanup (runs every hour)
276
+ setInterval(async () => {
277
+ try {
278
+ const cleaned = await continuationStore.cleanup();
279
+ if (cleaned > 0) {
280
+ debugLog(`ContinuationStore: Cleaned up ${cleaned} old conversations`);
281
+ }
282
+ } catch (error) {
283
+ debugError('ContinuationStore cleanup failed:', error);
284
+ }
285
+ }, 60 * 60 * 1000);
286
+ }
287
+ return continuationStore;
288
+ }
289
+
290
+ /**
291
+ * Set a custom continuation store backend (for testing or different implementations)
292
+ * @param {ContinuationStoreInterface} store - Custom store implementation
293
+ */
294
+ export function setContinuationStore(store) {
295
+ if (!(store instanceof ContinuationStoreInterface)) {
296
+ throw new ContinuationStoreError(
297
+ 'Store must extend ContinuationStoreInterface',
298
+ 'INVALID_STORE'
299
+ );
300
+ }
301
+ continuationStore = store;
302
+ }
303
+
304
+ /**
305
+ * Generate a new UUID-based continuation ID
306
+ * @returns {string} Unique continuation ID
307
+ */
308
+ export function generateContinuationId() {
309
+ return `conv_${randomUUID()}`;
310
+ }
311
+
312
+ /**
313
+ * Validate continuation ID format
314
+ * @param {string} continuationId - ID to validate
315
+ * @returns {boolean} True if valid format
316
+ */
317
+ export function isValidContinuationId(continuationId) {
318
+ if (!continuationId || typeof continuationId !== 'string') {
319
+ return false;
320
+ }
321
+
322
+ // Check for conv_ prefix and UUID format
323
+ const uuidPattern = /^conv_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
324
+ return uuidPattern.test(continuationId);
325
+ }
326
+
327
+ /**
328
+ * Helper function to add a message to conversation history
329
+ * @param {object} state - Current conversation state
330
+ * @param {object} message - Message to add
331
+ * @returns {object} Updated state
332
+ */
333
+ export function addMessageToHistory(state, message) {
334
+ const messages = state.messages || [];
335
+ return {
336
+ ...state,
337
+ messages: [...messages, message],
338
+ lastUpdated: Date.now(),
339
+ };
340
+ }
package/src/index.js ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Converse MCP Server - Main Entry Point
5
+ *
6
+ * Simplified, functional Node.js implementation of MCP server
7
+ * with chat and consensus tools using modern Node.js practices.
8
+ */
9
+
10
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { loadConfig, validateRuntimeConfig, getMcpClientConfig, getHttpTransportConfig } from './config.js';
13
+ import { createRouter } from './router.js';
14
+ import { createHTTPTransport } from './transport/httpTransport.js';
15
+ import { createLogger, startTimer } from './utils/logger.js';
16
+ import { debugError } from './utils/console.js';
17
+ import { ConfigurationError } from './utils/errorHandler.js';
18
+
19
+ const logger = createLogger('server');
20
+
21
+ /**
22
+ * Show help message
23
+ */
24
+ function showHelp() {
25
+ debugError(`
26
+ Converse MCP Server
27
+
28
+ Usage: node src/index.js [OPTIONS]
29
+
30
+ Options:
31
+ --transport <type> Transport type: http (default) or stdio
32
+ --transport=<type> Alternative format for transport type
33
+ --help Show this help message
34
+
35
+ Environment Variables:
36
+ MCP_TRANSPORT Transport type (http or stdio)
37
+ PORT HTTP server port (default: 3000)
38
+ HOST HTTP server host (default: localhost)
39
+
40
+ Examples:
41
+ node src/index.js # Start with HTTP transport (default)
42
+ node src/index.js --transport http # Start with HTTP transport
43
+ node src/index.js --transport stdio # Start with stdio transport
44
+ npm start # Start with HTTP transport
45
+ MCP_TRANSPORT=stdio npm start # Start with stdio transport
46
+ `);
47
+ }
48
+
49
+ /**
50
+ * Determine transport type from command line arguments or environment
51
+ */
52
+ function getTransportType() {
53
+ // Check command line arguments
54
+ const args = process.argv.slice(2);
55
+
56
+ // Support --transport=value format
57
+ const transportEqualArg = args.find(arg => arg.startsWith('--transport='));
58
+ if (transportEqualArg) {
59
+ const transport = transportEqualArg.split('=')[1];
60
+ if (transport && ['http', 'stdio'].includes(transport)) {
61
+ return transport;
62
+ }
63
+ }
64
+
65
+ // Support --transport value format
66
+ const transportIndex = args.findIndex(arg => arg === '--transport');
67
+ if (transportIndex >= 0 && transportIndex + 1 < args.length) {
68
+ const transport = args[transportIndex + 1];
69
+ if (transport && ['http', 'stdio'].includes(transport)) {
70
+ return transport;
71
+ }
72
+ }
73
+
74
+ // Check environment variable
75
+ if (process.env.MCP_TRANSPORT) {
76
+ const transport = process.env.MCP_TRANSPORT.toLowerCase();
77
+ if (['http', 'stdio'].includes(transport)) {
78
+ return transport;
79
+ }
80
+ }
81
+
82
+ // Default to HTTP for better development experience
83
+ return 'http';
84
+ }
85
+
86
+ async function main() {
87
+ // Check for help flag
88
+ const args = process.argv.slice(2);
89
+ if (args.includes('--help') || args.includes('-h')) {
90
+ showHelp();
91
+ process.exit(0);
92
+ }
93
+
94
+ const serverTimer = startTimer('server-startup', 'server');
95
+
96
+ try {
97
+ logger.info('Starting Converse MCP Server');
98
+
99
+ // Load and validate configuration
100
+ const config = await loadConfig();
101
+ await validateRuntimeConfig(config);
102
+
103
+ // Get MCP client configuration
104
+ const mcpConfig = getMcpClientConfig(config);
105
+
106
+ // Determine transport type
107
+ const transportType = getTransportType();
108
+ logger.info('Using transport type', { data: { transport: transportType } });
109
+
110
+ logger.debug('Creating MCP server instance', {
111
+ data: { name: mcpConfig.name, version: mcpConfig.version }
112
+ });
113
+
114
+ // Create MCP server with configuration
115
+ const server = new Server(
116
+ {
117
+ name: mcpConfig.name,
118
+ version: mcpConfig.version,
119
+ },
120
+ mcpConfig
121
+ );
122
+
123
+ // Set up router with server and config
124
+ await createRouter(server, config);
125
+
126
+ // Start server with appropriate transport
127
+ if (transportType === 'http') {
128
+ // HTTP streaming transport with full configuration
129
+ const httpConfig = getHttpTransportConfig(config);
130
+ const httpTransport = await createHTTPTransport(server, httpConfig);
131
+
132
+ await httpTransport.start();
133
+ const status = httpTransport.getStatus();
134
+
135
+ const startupTime = serverTimer('completed');
136
+ logger.info('Converse MCP Server started successfully with HTTP transport', {
137
+ data: {
138
+ startupTime: `${startupTime}ms`,
139
+ endpoint: `http://${status.host}:${status.port}/mcp`,
140
+ host: status.host,
141
+ port: status.port
142
+ }
143
+ });
144
+
145
+ // Store reference for shutdown
146
+ process.httpTransport = httpTransport;
147
+ } else {
148
+ // Stdio transport (legacy)
149
+ const transport = new StdioServerTransport();
150
+ await server.connect(transport);
151
+
152
+ const startupTime = serverTimer('completed');
153
+ logger.info('Converse MCP Server started successfully with stdio transport', {
154
+ data: { startupTime: `${startupTime}ms` }
155
+ });
156
+ }
157
+ } catch (error) {
158
+ serverTimer('failed');
159
+
160
+ if (error instanceof ConfigurationError) {
161
+ logger.error('Configuration error during startup', { error });
162
+ debugError('Configuration Error:');
163
+ debugError(error.message);
164
+ if (error.details?.errors) {
165
+ debugError('\nDetailed errors:');
166
+ error.details.errors.forEach(err => debugError(` - ${err}`));
167
+ }
168
+ process.exit(1);
169
+ } else {
170
+ logger.error('Failed to start Converse MCP Server', { error });
171
+ debugError('Failed to start Converse MCP Server:', error.message);
172
+ process.exit(1);
173
+ }
174
+ }
175
+ }
176
+
177
+ // Handle graceful shutdown
178
+ async function gracefulShutdown(signal) {
179
+ logger.info(`Received ${signal}, shutting down gracefully`);
180
+ debugError('Shutting down Converse MCP Server...');
181
+
182
+ if (process.httpTransport) {
183
+ try {
184
+ await process.httpTransport.stop();
185
+ logger.info('HTTP transport stopped successfully');
186
+ } catch (error) {
187
+ logger.error('Error stopping HTTP transport', { error });
188
+ }
189
+ }
190
+
191
+ process.exit(0);
192
+ }
193
+
194
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
195
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
196
+
197
+ process.on('uncaughtException', (error) => {
198
+ logger.error('Uncaught exception', { error });
199
+ debugError('Fatal error:', error);
200
+ process.exit(1);
201
+ });
202
+
203
+ process.on('unhandledRejection', (reason, promise) => {
204
+ logger.error('Unhandled promise rejection', {
205
+ error: reason,
206
+ data: { promise: promise.toString() }
207
+ });
208
+ debugError('Unhandled promise rejection:', reason);
209
+ process.exit(1);
210
+ });
211
+
212
+ main().catch((error) => {
213
+ logger.error('Fatal error in main', { error });
214
+ debugError('Fatal error:', error);
215
+ process.exit(1);
216
+ });