cellium-mcp-client 1.1.3 → 1.1.4
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/dist/cli.js +63 -15
- package/dist/client.d.ts +11 -0
- package/dist/client.js +412 -92
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,23 +11,32 @@ const program = new commander_1.Command();
|
|
|
11
11
|
program
|
|
12
12
|
.name('cellium-mcp-client')
|
|
13
13
|
.description('MCP client for connecting to remote Cellium processor server')
|
|
14
|
-
.version('1.1.
|
|
14
|
+
.version('1.1.3')
|
|
15
15
|
.option('-t, --token <token>', 'Authentication token (format: user:username:hash)')
|
|
16
16
|
.option('-e, --endpoint <url>', 'Server endpoint URL', 'http://localhost:3000/mcp')
|
|
17
17
|
.option('-v, --verbose', 'Enable verbose logging')
|
|
18
|
+
.option('-d, --debug', 'Enable debug mode with extensive logging')
|
|
18
19
|
.option('-r, --retry-attempts <num>', 'Number of retry attempts on connection failure', '3')
|
|
19
20
|
.option('--retry-delay <ms>', 'Delay between retry attempts in milliseconds', '1000')
|
|
20
21
|
.parse(process.argv);
|
|
21
22
|
const options = program.opts();
|
|
22
|
-
// Configure logging
|
|
23
|
+
// Configure logging with different levels based on options
|
|
24
|
+
let logLevel = 'info';
|
|
25
|
+
if (options.debug) {
|
|
26
|
+
logLevel = 'debug';
|
|
27
|
+
}
|
|
28
|
+
else if (options.verbose) {
|
|
29
|
+
logLevel = 'debug';
|
|
30
|
+
}
|
|
23
31
|
const logger = (0, pino_1.default)({
|
|
24
|
-
level:
|
|
32
|
+
level: logLevel,
|
|
25
33
|
transport: {
|
|
26
34
|
target: 'pino-pretty',
|
|
27
35
|
options: {
|
|
28
36
|
colorize: true,
|
|
29
|
-
translateTime: 'HH:MM:ss',
|
|
30
|
-
ignore: 'pid,hostname'
|
|
37
|
+
translateTime: 'HH:MM:ss.l',
|
|
38
|
+
ignore: 'pid,hostname',
|
|
39
|
+
messageFormat: options.debug ? '[{level}] {msg}' : '{msg}'
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
});
|
|
@@ -37,47 +46,86 @@ async function main() {
|
|
|
37
46
|
const token = options.token || process.env.CELLIUM_MCP_TOKEN;
|
|
38
47
|
if (!token) {
|
|
39
48
|
logger.error('Authentication token required. Use --token option or CELLIUM_MCP_TOKEN environment variable');
|
|
49
|
+
logger.info('Token format should be: user:username:hash');
|
|
40
50
|
process.exit(1);
|
|
41
51
|
}
|
|
42
52
|
// Validate token format
|
|
43
53
|
if (!token.match(/^user:[^:]+:[a-f0-9]+$/)) {
|
|
44
54
|
logger.error('Invalid token format. Expected: user:username:hash');
|
|
55
|
+
logger.info('Example: user:myusername:abc123def456...');
|
|
45
56
|
process.exit(1);
|
|
46
57
|
}
|
|
47
|
-
logger.info(
|
|
58
|
+
logger.info({
|
|
59
|
+
version: '1.1.3',
|
|
60
|
+
debugMode: !!options.debug,
|
|
61
|
+
verboseMode: !!options.verbose
|
|
62
|
+
}, 'Starting Cellium MCP Client');
|
|
48
63
|
logger.debug({
|
|
49
64
|
endpoint: options.endpoint,
|
|
50
65
|
retryAttempts: parseInt(options.retryAttempts),
|
|
51
|
-
retryDelay: parseInt(options.retryDelay)
|
|
52
|
-
|
|
66
|
+
retryDelay: parseInt(options.retryDelay),
|
|
67
|
+
tokenPreview: `${token.split(':')[0]}:${token.split(':')[1]}:${token.split(':')[2]?.substring(0, 8)}...`
|
|
68
|
+
}, 'Configuration loaded');
|
|
53
69
|
const client = new client_1.CelliumMCPClient({
|
|
54
70
|
token,
|
|
55
71
|
endpoint: options.endpoint,
|
|
56
72
|
logger,
|
|
57
73
|
retryAttempts: parseInt(options.retryAttempts),
|
|
58
|
-
retryDelay: parseInt(options.retryDelay)
|
|
74
|
+
retryDelay: parseInt(options.retryDelay),
|
|
75
|
+
debugMode: !!options.debug
|
|
76
|
+
});
|
|
77
|
+
// Enhanced error handling for unhandled rejections
|
|
78
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
79
|
+
logger.error({
|
|
80
|
+
reason,
|
|
81
|
+
promise
|
|
82
|
+
}, 'Unhandled promise rejection');
|
|
83
|
+
});
|
|
84
|
+
process.on('uncaughtException', (error) => {
|
|
85
|
+
logger.fatal({ error }, 'Uncaught exception');
|
|
86
|
+
process.exit(1);
|
|
59
87
|
});
|
|
60
88
|
// Handle graceful shutdown
|
|
61
89
|
process.on('SIGINT', async () => {
|
|
62
90
|
logger.info('Received SIGINT, shutting down gracefully');
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
try {
|
|
92
|
+
await client.disconnect();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
logger.error({ error }, 'Error during shutdown');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
65
99
|
});
|
|
66
100
|
process.on('SIGTERM', async () => {
|
|
67
101
|
logger.info('Received SIGTERM, shutting down gracefully');
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
try {
|
|
103
|
+
await client.disconnect();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.error({ error }, 'Error during shutdown');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
70
110
|
});
|
|
111
|
+
logger.debug('Connecting to MCP transport...');
|
|
71
112
|
await client.connect();
|
|
113
|
+
logger.info('MCP Client ready - starting server...');
|
|
72
114
|
// Start the MCP server and keep process alive for stdio communication
|
|
73
115
|
await client.serve();
|
|
74
116
|
}
|
|
75
117
|
catch (error) {
|
|
76
|
-
logger.
|
|
118
|
+
logger.fatal({
|
|
119
|
+
error: error instanceof Error ? {
|
|
120
|
+
name: error.name,
|
|
121
|
+
message: error.message,
|
|
122
|
+
stack: error.stack
|
|
123
|
+
} : error
|
|
124
|
+
}, 'Fatal error during startup');
|
|
77
125
|
process.exit(1);
|
|
78
126
|
}
|
|
79
127
|
}
|
|
80
128
|
main().catch((error) => {
|
|
81
|
-
console.error('Unhandled error:', error);
|
|
129
|
+
console.error('Unhandled startup error:', error);
|
|
82
130
|
process.exit(1);
|
|
83
131
|
});
|
package/dist/client.d.ts
CHANGED
|
@@ -5,17 +5,28 @@ export interface CelliumMCPClientConfig {
|
|
|
5
5
|
logger: Logger;
|
|
6
6
|
retryAttempts?: number;
|
|
7
7
|
retryDelay?: number;
|
|
8
|
+
debugMode?: boolean;
|
|
8
9
|
}
|
|
9
10
|
export declare class CelliumMCPClient {
|
|
10
11
|
private config;
|
|
11
12
|
private localServer;
|
|
13
|
+
private transport?;
|
|
12
14
|
private isConnected;
|
|
13
15
|
private reconnectTimer?;
|
|
14
16
|
private keepAliveInterval?;
|
|
17
|
+
private transportState;
|
|
18
|
+
private activeRequests;
|
|
19
|
+
private mcpProtocolVersion;
|
|
15
20
|
constructor(config: CelliumMCPClientConfig);
|
|
21
|
+
private logDebug;
|
|
22
|
+
private logRequest;
|
|
23
|
+
private logResponse;
|
|
24
|
+
private setupTransportMonitoring;
|
|
16
25
|
private setupServer;
|
|
26
|
+
private getSafeErrorResponse;
|
|
17
27
|
private makeHttpRequest;
|
|
18
28
|
connect(): Promise<void>;
|
|
29
|
+
private setupTransportEventListeners;
|
|
19
30
|
private testConnectionInBackground;
|
|
20
31
|
serve(): Promise<void>;
|
|
21
32
|
private testConnection;
|
package/dist/client.js
CHANGED
|
@@ -44,19 +44,33 @@ const InitializedNotificationSchema = zod_1.z.object({
|
|
|
44
44
|
class CelliumMCPClient {
|
|
45
45
|
config;
|
|
46
46
|
localServer;
|
|
47
|
+
transport;
|
|
47
48
|
isConnected = false;
|
|
48
49
|
reconnectTimer;
|
|
49
50
|
keepAliveInterval;
|
|
51
|
+
transportState = {
|
|
52
|
+
connected: false,
|
|
53
|
+
lastActivity: 0,
|
|
54
|
+
requestCount: 0,
|
|
55
|
+
errorCount: 0
|
|
56
|
+
};
|
|
57
|
+
activeRequests = new Map();
|
|
58
|
+
mcpProtocolVersion = '2024-11-05';
|
|
50
59
|
constructor(config) {
|
|
51
60
|
this.config = {
|
|
52
61
|
retryAttempts: 3,
|
|
53
62
|
retryDelay: 1000,
|
|
63
|
+
debugMode: false,
|
|
54
64
|
...config
|
|
55
65
|
};
|
|
66
|
+
this.logDebug('Initializing CelliumMCPClient', {
|
|
67
|
+
endpoint: this.config.endpoint,
|
|
68
|
+
debugMode: this.config.debugMode
|
|
69
|
+
});
|
|
56
70
|
// Local server that interfaces with AI assistants via stdio
|
|
57
71
|
this.localServer = new mcp_js_1.McpServer({
|
|
58
72
|
name: 'cellium-mcp-client',
|
|
59
|
-
version: '1.
|
|
73
|
+
version: '1.1.3'
|
|
60
74
|
}, {
|
|
61
75
|
capabilities: {
|
|
62
76
|
tools: {},
|
|
@@ -64,172 +78,422 @@ class CelliumMCPClient {
|
|
|
64
78
|
}
|
|
65
79
|
});
|
|
66
80
|
this.setupServer();
|
|
81
|
+
this.setupTransportMonitoring();
|
|
82
|
+
}
|
|
83
|
+
logDebug(message, data) {
|
|
84
|
+
if (this.config.debugMode) {
|
|
85
|
+
this.config.logger.debug({
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
transportState: this.transportState,
|
|
88
|
+
activeRequests: Array.from(this.activeRequests.entries()),
|
|
89
|
+
...data
|
|
90
|
+
}, `[CELLIUM-MCP-DEBUG] ${message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
logRequest(method, id, params) {
|
|
94
|
+
this.transportState.requestCount++;
|
|
95
|
+
this.transportState.lastActivity = Date.now();
|
|
96
|
+
const timing = {
|
|
97
|
+
start: Date.now(),
|
|
98
|
+
method,
|
|
99
|
+
id
|
|
100
|
+
};
|
|
101
|
+
this.activeRequests.set(id, timing);
|
|
102
|
+
this.config.logger.info({
|
|
103
|
+
requestId: id,
|
|
104
|
+
method,
|
|
105
|
+
params,
|
|
106
|
+
requestCount: this.transportState.requestCount,
|
|
107
|
+
activeRequestCount: this.activeRequests.size
|
|
108
|
+
}, `[MCP-REQUEST] Received ${method}`);
|
|
109
|
+
this.logDebug(`Request received: ${method}`, { id, params });
|
|
110
|
+
}
|
|
111
|
+
logResponse(method, id, success, result, error) {
|
|
112
|
+
const timing = this.activeRequests.get(id);
|
|
113
|
+
const duration = timing ? Date.now() - timing.start : 0;
|
|
114
|
+
this.activeRequests.delete(id);
|
|
115
|
+
this.transportState.lastActivity = Date.now();
|
|
116
|
+
if (!success) {
|
|
117
|
+
this.transportState.errorCount++;
|
|
118
|
+
}
|
|
119
|
+
this.config.logger.info({
|
|
120
|
+
requestId: id,
|
|
121
|
+
method,
|
|
122
|
+
success,
|
|
123
|
+
duration,
|
|
124
|
+
result: success ? result : undefined,
|
|
125
|
+
error: !success ? error : undefined,
|
|
126
|
+
activeRequestCount: this.activeRequests.size,
|
|
127
|
+
totalErrors: this.transportState.errorCount
|
|
128
|
+
}, `[MCP-RESPONSE] Completed ${method} in ${duration}ms`);
|
|
129
|
+
this.logDebug(`Response sent: ${method}`, {
|
|
130
|
+
id,
|
|
131
|
+
success,
|
|
132
|
+
duration,
|
|
133
|
+
result: success ? result : undefined,
|
|
134
|
+
error: !success ? error : undefined
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
setupTransportMonitoring() {
|
|
138
|
+
// Monitor transport health every 10 seconds
|
|
139
|
+
setInterval(() => {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const timeSinceLastActivity = now - this.transportState.lastActivity;
|
|
142
|
+
this.logDebug('Transport health check', {
|
|
143
|
+
timeSinceLastActivity,
|
|
144
|
+
activeRequestsCount: this.activeRequests.size,
|
|
145
|
+
errorRate: this.transportState.errorCount / Math.max(this.transportState.requestCount, 1)
|
|
146
|
+
});
|
|
147
|
+
// Log warning if no activity for 2 minutes
|
|
148
|
+
if (timeSinceLastActivity > 120000 && this.transportState.requestCount > 0) {
|
|
149
|
+
this.config.logger.warn({
|
|
150
|
+
timeSinceLastActivity,
|
|
151
|
+
lastActivity: new Date(this.transportState.lastActivity).toISOString()
|
|
152
|
+
}, 'Transport appears idle for extended period');
|
|
153
|
+
}
|
|
154
|
+
// Log active requests that are taking too long (>30s)
|
|
155
|
+
for (const [id, timing] of this.activeRequests.entries()) {
|
|
156
|
+
const requestDuration = now - timing.start;
|
|
157
|
+
if (requestDuration > 30000) {
|
|
158
|
+
this.config.logger.warn({
|
|
159
|
+
requestId: id,
|
|
160
|
+
method: timing.method,
|
|
161
|
+
duration: requestDuration
|
|
162
|
+
}, 'Long-running request detected');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}, 10000);
|
|
67
166
|
}
|
|
68
167
|
setupServer() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
|
|
168
|
+
this.logDebug('Setting up MCP server handlers');
|
|
169
|
+
// Add error boundary wrapper for all handlers
|
|
170
|
+
const withErrorBoundary = (handler, methodName) => {
|
|
171
|
+
return async (request) => {
|
|
172
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
173
|
+
try {
|
|
174
|
+
this.logRequest(methodName, requestId, request);
|
|
175
|
+
const result = await handler(request);
|
|
176
|
+
this.logResponse(methodName, requestId, true, result);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
this.logResponse(methodName, requestId, false, undefined, error);
|
|
181
|
+
// Log the full error for debugging
|
|
182
|
+
this.config.logger.error({
|
|
183
|
+
error: error instanceof Error ? {
|
|
184
|
+
name: error.name,
|
|
185
|
+
message: error.message,
|
|
186
|
+
stack: error.stack
|
|
187
|
+
} : error,
|
|
188
|
+
methodName,
|
|
189
|
+
requestId,
|
|
190
|
+
request
|
|
191
|
+
}, `Error in ${methodName} handler`);
|
|
192
|
+
// Don't re-throw - return safe fallback instead to prevent transport closure
|
|
193
|
+
return this.getSafeErrorResponse(methodName, error);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
// Handle MCP initialization with protocol version checking
|
|
198
|
+
this.localServer.server.setRequestHandler(InitializeSchema, withErrorBoundary(async (request) => {
|
|
199
|
+
this.logDebug('Processing initialize request', request);
|
|
200
|
+
const clientProtocolVersion = request.params?.protocolVersion;
|
|
201
|
+
if (clientProtocolVersion && clientProtocolVersion !== this.mcpProtocolVersion) {
|
|
202
|
+
this.config.logger.warn({
|
|
203
|
+
clientVersion: clientProtocolVersion,
|
|
204
|
+
serverVersion: this.mcpProtocolVersion
|
|
205
|
+
}, 'Protocol version mismatch detected');
|
|
206
|
+
}
|
|
207
|
+
const initResponse = {
|
|
208
|
+
protocolVersion: this.mcpProtocolVersion,
|
|
74
209
|
capabilities: {
|
|
75
210
|
tools: {},
|
|
76
211
|
resources: {}
|
|
77
212
|
},
|
|
78
213
|
serverInfo: {
|
|
79
214
|
name: 'cellium-mcp-client',
|
|
80
|
-
version: '1.1.
|
|
215
|
+
version: '1.1.3'
|
|
81
216
|
}
|
|
82
217
|
};
|
|
83
|
-
|
|
218
|
+
this.transportState.connected = true;
|
|
219
|
+
this.transportState.lastActivity = Date.now();
|
|
220
|
+
this.logDebug('Initialize response prepared', initResponse);
|
|
221
|
+
return initResponse;
|
|
222
|
+
}, 'initialize'));
|
|
84
223
|
// Override the underlying server's tool request handlers to proxy to remote
|
|
85
|
-
this.localServer.server.setRequestHandler(ToolsListSchema, async () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
this.config.logger.error({ error }, 'Error proxying tools/list');
|
|
94
|
-
// Return empty tools list instead of throwing to prevent transport closure
|
|
95
|
-
return { tools: [] };
|
|
224
|
+
this.localServer.server.setRequestHandler(ToolsListSchema, withErrorBoundary(async (request) => {
|
|
225
|
+
this.logDebug('Processing tools/list request', request);
|
|
226
|
+
// Add artificial delay to test timing issues
|
|
227
|
+
if (this.config.debugMode) {
|
|
228
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
96
229
|
}
|
|
230
|
+
const result = await this.makeHttpRequest('tools/list', {});
|
|
231
|
+
this.logDebug('tools/list completed successfully', { toolCount: result.tools?.length || 0 });
|
|
232
|
+
return result;
|
|
233
|
+
}, 'tools/list'));
|
|
234
|
+
this.localServer.server.setRequestHandler(ToolsCallSchema, withErrorBoundary(async (request) => {
|
|
235
|
+
this.logDebug('Processing tools/call request', {
|
|
236
|
+
toolName: request.params?.name,
|
|
237
|
+
hasArguments: !!request.params?.arguments
|
|
238
|
+
});
|
|
239
|
+
const result = await this.makeHttpRequest('tools/call', request.params);
|
|
240
|
+
this.logDebug('tools/call completed successfully', {
|
|
241
|
+
toolName: request.params?.name,
|
|
242
|
+
resultType: typeof result
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
245
|
+
}, 'tools/call'));
|
|
246
|
+
// Handle resources as well
|
|
247
|
+
this.localServer.server.setRequestHandler(ResourcesListSchema, withErrorBoundary(async (request) => {
|
|
248
|
+
this.logDebug('Processing resources/list request', request);
|
|
249
|
+
const result = await this.makeHttpRequest('resources/list', {});
|
|
250
|
+
this.logDebug('resources/list completed successfully', { resourceCount: result.resources?.length || 0 });
|
|
251
|
+
return result;
|
|
252
|
+
}, 'resources/list'));
|
|
253
|
+
this.localServer.server.setRequestHandler(ResourcesReadSchema, withErrorBoundary(async (request) => {
|
|
254
|
+
this.logDebug('Processing resources/read request', { uri: request.params?.uri });
|
|
255
|
+
const result = await this.makeHttpRequest('resources/read', request.params);
|
|
256
|
+
this.logDebug('resources/read completed successfully', { uri: request.params?.uri });
|
|
257
|
+
return result;
|
|
258
|
+
}, 'resources/read'));
|
|
259
|
+
// Handle ping
|
|
260
|
+
this.localServer.server.setRequestHandler(PingSchema, withErrorBoundary(async (request) => {
|
|
261
|
+
this.logDebug('Processing ping request', request);
|
|
262
|
+
const result = await this.makeHttpRequest('ping', {});
|
|
263
|
+
this.logDebug('ping completed successfully');
|
|
264
|
+
return result;
|
|
265
|
+
}, 'ping'));
|
|
266
|
+
// Handle other common MCP methods
|
|
267
|
+
this.localServer.server.setNotificationHandler(InitializedNotificationSchema, async (notification) => {
|
|
268
|
+
const notificationId = Math.random().toString(36).substring(2, 15);
|
|
269
|
+
this.logRequest('notifications/initialized', notificationId, notification);
|
|
270
|
+
this.logDebug('Received initialized notification', notification);
|
|
271
|
+
this.transportState.connected = true;
|
|
272
|
+
this.transportState.lastActivity = Date.now();
|
|
273
|
+
this.logResponse('notifications/initialized', notificationId, true);
|
|
274
|
+
// No response needed for notifications
|
|
97
275
|
});
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
276
|
+
this.logDebug('MCP server handlers setup completed');
|
|
277
|
+
}
|
|
278
|
+
getSafeErrorResponse(methodName, error) {
|
|
279
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
280
|
+
switch (methodName) {
|
|
281
|
+
case 'tools/list':
|
|
282
|
+
return { tools: [] };
|
|
283
|
+
case 'resources/list':
|
|
284
|
+
return { resources: [] };
|
|
285
|
+
case 'tools/call':
|
|
107
286
|
return {
|
|
108
287
|
content: [{
|
|
109
288
|
type: 'text',
|
|
110
|
-
text: `Error calling tool: ${
|
|
289
|
+
text: `Error calling tool: ${errorMessage}`
|
|
111
290
|
}],
|
|
112
291
|
isError: true
|
|
113
292
|
};
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
// Handle resources as well
|
|
117
|
-
this.localServer.server.setRequestHandler(ResourcesListSchema, async () => {
|
|
118
|
-
try {
|
|
119
|
-
this.config.logger.debug('Proxying resources/list to remote server');
|
|
120
|
-
const result = await this.makeHttpRequest('resources/list', {});
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
this.config.logger.error({ error }, 'Error proxying resources/list');
|
|
125
|
-
// Return empty resources list instead of throwing
|
|
126
|
-
return { resources: [] };
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
this.localServer.server.setRequestHandler(ResourcesReadSchema, async (request) => {
|
|
130
|
-
try {
|
|
131
|
-
this.config.logger.debug({ uri: request.params?.uri }, 'Proxying resources/read to remote server');
|
|
132
|
-
const result = await this.makeHttpRequest('resources/read', request.params);
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
this.config.logger.error({ error, uri: request.params?.uri }, 'Error proxying resources/read');
|
|
137
|
-
// Return error result instead of throwing
|
|
293
|
+
case 'resources/read':
|
|
138
294
|
return {
|
|
139
295
|
contents: [{
|
|
140
|
-
uri:
|
|
296
|
+
uri: '',
|
|
141
297
|
mimeType: 'text/plain',
|
|
142
|
-
text: `Error reading resource: ${
|
|
298
|
+
text: `Error reading resource: ${errorMessage}`
|
|
143
299
|
}]
|
|
144
300
|
};
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
// Handle ping
|
|
148
|
-
this.localServer.server.setRequestHandler(PingSchema, async () => {
|
|
149
|
-
try {
|
|
150
|
-
const result = await this.makeHttpRequest('ping', {});
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
this.config.logger.error({ error }, 'Error proxying ping');
|
|
155
|
-
// Return empty result instead of throwing
|
|
301
|
+
case 'ping':
|
|
156
302
|
return {};
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.localServer.server.setNotificationHandler(InitializedNotificationSchema, async () => {
|
|
161
|
-
this.config.logger.debug('Received initialized notification');
|
|
162
|
-
// No response needed for notifications
|
|
163
|
-
});
|
|
303
|
+
default:
|
|
304
|
+
return { error: errorMessage };
|
|
305
|
+
}
|
|
164
306
|
}
|
|
165
307
|
async makeHttpRequest(method, params) {
|
|
308
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
309
|
+
const startTime = Date.now();
|
|
310
|
+
this.logDebug(`Starting HTTP request for ${method}`, {
|
|
311
|
+
requestId,
|
|
312
|
+
params,
|
|
313
|
+
isConnected: this.isConnected,
|
|
314
|
+
endpoint: this.config.endpoint
|
|
315
|
+
});
|
|
166
316
|
// If not connected, try to connect first
|
|
167
317
|
if (!this.isConnected) {
|
|
168
318
|
try {
|
|
319
|
+
this.logDebug('Not connected, attempting connection test');
|
|
169
320
|
await this.testConnection();
|
|
170
321
|
this.isConnected = true;
|
|
171
322
|
this.config.logger.info('Connected to remote Cellium server');
|
|
172
323
|
}
|
|
173
324
|
catch (error) {
|
|
174
|
-
this.config.logger.error({
|
|
325
|
+
this.config.logger.error({
|
|
326
|
+
error: error instanceof Error ? {
|
|
327
|
+
name: error.name,
|
|
328
|
+
message: error.message,
|
|
329
|
+
stack: error.stack
|
|
330
|
+
} : error
|
|
331
|
+
}, 'Failed to connect to remote server');
|
|
175
332
|
throw new Error('Cannot connect to remote Cellium server');
|
|
176
333
|
}
|
|
177
334
|
}
|
|
178
335
|
const mcpEndpoint = this.config.endpoint.replace('/sse', '/mcp');
|
|
179
336
|
const requestBody = {
|
|
180
337
|
jsonrpc: '2.0',
|
|
181
|
-
id:
|
|
338
|
+
id: requestId,
|
|
182
339
|
method,
|
|
183
340
|
params
|
|
184
341
|
};
|
|
185
|
-
this.
|
|
342
|
+
this.logDebug(`Making HTTP request to ${mcpEndpoint}`, {
|
|
343
|
+
requestId,
|
|
344
|
+
method,
|
|
345
|
+
bodySize: JSON.stringify(requestBody).length
|
|
346
|
+
});
|
|
347
|
+
let response;
|
|
348
|
+
let responseText;
|
|
186
349
|
try {
|
|
187
|
-
|
|
350
|
+
response = await fetch(mcpEndpoint, {
|
|
188
351
|
method: 'POST',
|
|
189
352
|
headers: {
|
|
190
353
|
'Authorization': `Bearer ${this.config.token}`,
|
|
191
|
-
'Content-Type': 'application/json'
|
|
354
|
+
'Content-Type': 'application/json',
|
|
355
|
+
'User-Agent': 'cellium-mcp-client/1.1.3'
|
|
192
356
|
},
|
|
193
357
|
body: JSON.stringify(requestBody)
|
|
194
358
|
});
|
|
359
|
+
responseText = await response.text();
|
|
360
|
+
const duration = Date.now() - startTime;
|
|
361
|
+
this.logDebug('HTTP response received', {
|
|
362
|
+
requestId,
|
|
363
|
+
method,
|
|
364
|
+
status: response.status,
|
|
365
|
+
statusText: response.statusText,
|
|
366
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
367
|
+
responseSize: responseText.length,
|
|
368
|
+
duration
|
|
369
|
+
});
|
|
195
370
|
if (!response.ok) {
|
|
196
371
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
197
372
|
}
|
|
198
|
-
|
|
373
|
+
let jsonResponse;
|
|
374
|
+
try {
|
|
375
|
+
jsonResponse = JSON.parse(responseText);
|
|
376
|
+
}
|
|
377
|
+
catch (parseError) {
|
|
378
|
+
this.config.logger.error({
|
|
379
|
+
requestId,
|
|
380
|
+
method,
|
|
381
|
+
responseText: responseText.substring(0, 500),
|
|
382
|
+
parseError: parseError instanceof Error ? parseError.message : parseError
|
|
383
|
+
}, 'Failed to parse JSON response');
|
|
384
|
+
throw new Error('Invalid JSON response from server');
|
|
385
|
+
}
|
|
386
|
+
this.logDebug('HTTP response parsed', {
|
|
387
|
+
requestId,
|
|
388
|
+
method,
|
|
389
|
+
hasResult: !!jsonResponse.result,
|
|
390
|
+
hasError: !!jsonResponse.error,
|
|
391
|
+
jsonrpcId: jsonResponse.id
|
|
392
|
+
});
|
|
199
393
|
if (jsonResponse.error) {
|
|
394
|
+
this.config.logger.error({
|
|
395
|
+
requestId,
|
|
396
|
+
method,
|
|
397
|
+
serverError: jsonResponse.error
|
|
398
|
+
}, 'Remote server returned error');
|
|
200
399
|
throw new Error(`Remote server error: ${jsonResponse.error.message}`);
|
|
201
400
|
}
|
|
202
401
|
return jsonResponse.result;
|
|
203
402
|
}
|
|
204
403
|
catch (error) {
|
|
205
|
-
|
|
404
|
+
const duration = Date.now() - startTime;
|
|
405
|
+
this.config.logger.error({
|
|
406
|
+
error: error instanceof Error ? {
|
|
407
|
+
name: error.name,
|
|
408
|
+
message: error.message,
|
|
409
|
+
stack: error.stack
|
|
410
|
+
} : error,
|
|
411
|
+
requestId,
|
|
412
|
+
method,
|
|
413
|
+
duration,
|
|
414
|
+
endpoint: mcpEndpoint
|
|
415
|
+
}, 'HTTP request to remote server failed');
|
|
206
416
|
this.isConnected = false; // Mark as disconnected on error
|
|
207
417
|
throw error;
|
|
208
418
|
}
|
|
209
419
|
}
|
|
210
420
|
async connect() {
|
|
211
421
|
try {
|
|
212
|
-
this.config.logger.info({
|
|
422
|
+
this.config.logger.info({
|
|
423
|
+
endpoint: this.config.endpoint,
|
|
424
|
+
debugMode: this.config.debugMode,
|
|
425
|
+
protocolVersion: this.mcpProtocolVersion
|
|
426
|
+
}, 'Starting Cellium MCP Server');
|
|
427
|
+
this.logDebug('Initializing stdio transport');
|
|
213
428
|
// Set up the stdio transport for local MCP server immediately
|
|
214
|
-
|
|
215
|
-
|
|
429
|
+
this.transport = new stdio_js_1.StdioServerTransport();
|
|
430
|
+
// Add transport event monitoring
|
|
431
|
+
this.setupTransportEventListeners(this.transport);
|
|
432
|
+
await this.localServer.connect(this.transport);
|
|
433
|
+
this.transportState.connected = true;
|
|
434
|
+
this.transportState.lastActivity = Date.now();
|
|
216
435
|
this.config.logger.info('MCP Server connected and ready');
|
|
436
|
+
this.logDebug('Transport connected successfully', {
|
|
437
|
+
transportType: 'stdio',
|
|
438
|
+
serverName: 'cellium-mcp-client',
|
|
439
|
+
serverVersion: '1.1.3'
|
|
440
|
+
});
|
|
217
441
|
// Keep the process alive with a minimal interval
|
|
218
442
|
// This ensures the process doesn't exit when stdin closes
|
|
219
443
|
this.keepAliveInterval = setInterval(() => {
|
|
220
|
-
|
|
444
|
+
this.logDebug('Keep-alive tick', {
|
|
445
|
+
uptime: Date.now() - this.transportState.lastActivity,
|
|
446
|
+
activeRequests: this.activeRequests.size
|
|
447
|
+
});
|
|
221
448
|
}, 30000); // Check every 30 seconds
|
|
222
449
|
this.config.logger.debug('Keep-alive interval started for persistent MCP communication');
|
|
223
450
|
// Test connection to remote server in background, but don't block startup
|
|
224
451
|
this.testConnectionInBackground();
|
|
225
452
|
}
|
|
226
453
|
catch (error) {
|
|
227
|
-
this.config.logger.error({
|
|
454
|
+
this.config.logger.error({
|
|
455
|
+
error: error instanceof Error ? {
|
|
456
|
+
name: error.name,
|
|
457
|
+
message: error.message,
|
|
458
|
+
stack: error.stack
|
|
459
|
+
} : error
|
|
460
|
+
}, 'Failed to start MCP server');
|
|
461
|
+
this.transportState.connected = false;
|
|
228
462
|
throw error;
|
|
229
463
|
}
|
|
230
464
|
}
|
|
465
|
+
setupTransportEventListeners(transport) {
|
|
466
|
+
this.logDebug('Setting up transport event listeners');
|
|
467
|
+
// Monitor transport events if available
|
|
468
|
+
try {
|
|
469
|
+
// Try to access transport events (may not be available in all SDK versions)
|
|
470
|
+
const transportAny = transport;
|
|
471
|
+
if (transportAny.on && typeof transportAny.on === 'function') {
|
|
472
|
+
transportAny.on('close', () => {
|
|
473
|
+
this.config.logger.warn('Transport close event detected');
|
|
474
|
+
this.transportState.connected = false;
|
|
475
|
+
});
|
|
476
|
+
transportAny.on('error', (error) => {
|
|
477
|
+
this.config.logger.error({ error }, 'Transport error event');
|
|
478
|
+
this.transportState.errorCount++;
|
|
479
|
+
});
|
|
480
|
+
transportAny.on('connect', () => {
|
|
481
|
+
this.config.logger.info('Transport connect event');
|
|
482
|
+
this.transportState.connected = true;
|
|
483
|
+
});
|
|
484
|
+
this.logDebug('Transport event listeners attached');
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
this.logDebug('Transport does not support event listeners');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
this.logDebug('Could not attach transport event listeners', { error });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
231
494
|
async testConnectionInBackground() {
|
|
232
495
|
try {
|
|
496
|
+
this.logDebug('Testing connection to remote server in background');
|
|
233
497
|
await this.testConnection();
|
|
234
498
|
this.isConnected = true;
|
|
235
499
|
this.config.logger.info('Connected to remote Cellium server');
|
|
@@ -239,7 +503,9 @@ class CelliumMCPClient {
|
|
|
239
503
|
}
|
|
240
504
|
}
|
|
241
505
|
catch (error) {
|
|
242
|
-
this.config.logger.warn({
|
|
506
|
+
this.config.logger.warn({
|
|
507
|
+
error: error instanceof Error ? error.message : error
|
|
508
|
+
}, 'Failed to connect to remote server, will retry on first request');
|
|
243
509
|
this.isConnected = false;
|
|
244
510
|
}
|
|
245
511
|
}
|
|
@@ -247,7 +513,22 @@ class CelliumMCPClient {
|
|
|
247
513
|
// The MCP server is already connected via stdio in connect()
|
|
248
514
|
// Keep the process alive indefinitely for persistent MCP communication
|
|
249
515
|
// This ensures compatibility with MCP clients like Copilot that expect long-running servers
|
|
250
|
-
this.config.logger.debug(
|
|
516
|
+
this.config.logger.debug({
|
|
517
|
+
transportConnected: this.transportState.connected,
|
|
518
|
+
serverName: 'cellium-mcp-client'
|
|
519
|
+
}, 'Server is now serving and will stay alive for persistent MCP communication');
|
|
520
|
+
this.logDebug('Entering serve mode - process will stay alive');
|
|
521
|
+
// Set up graceful shutdown handlers
|
|
522
|
+
process.on('SIGINT', async () => {
|
|
523
|
+
this.config.logger.info('Received SIGINT, shutting down gracefully...');
|
|
524
|
+
await this.disconnect();
|
|
525
|
+
process.exit(0);
|
|
526
|
+
});
|
|
527
|
+
process.on('SIGTERM', async () => {
|
|
528
|
+
this.config.logger.info('Received SIGTERM, shutting down gracefully...');
|
|
529
|
+
await this.disconnect();
|
|
530
|
+
process.exit(0);
|
|
531
|
+
});
|
|
251
532
|
// Return a promise that never resolves to keep the process alive
|
|
252
533
|
// The process will only exit via SIGINT/SIGTERM signals
|
|
253
534
|
return new Promise(() => {
|
|
@@ -256,20 +537,32 @@ class CelliumMCPClient {
|
|
|
256
537
|
});
|
|
257
538
|
}
|
|
258
539
|
async testConnection() {
|
|
540
|
+
const startTime = Date.now();
|
|
259
541
|
const mcpEndpoint = this.config.endpoint.replace('/sse', '/mcp');
|
|
542
|
+
this.logDebug('Testing connection', {
|
|
543
|
+
endpoint: mcpEndpoint,
|
|
544
|
+
hasToken: !!this.config.token
|
|
545
|
+
});
|
|
260
546
|
const response = await fetch(mcpEndpoint, {
|
|
261
547
|
method: 'POST',
|
|
262
548
|
headers: {
|
|
263
549
|
'Authorization': `Bearer ${this.config.token}`,
|
|
264
|
-
'Content-Type': 'application/json'
|
|
550
|
+
'Content-Type': 'application/json',
|
|
551
|
+
'User-Agent': 'cellium-mcp-client/1.1.3'
|
|
265
552
|
},
|
|
266
553
|
body: JSON.stringify({
|
|
267
554
|
jsonrpc: '2.0',
|
|
268
|
-
id: 'test
|
|
555
|
+
id: 'connection-test',
|
|
269
556
|
method: 'ping',
|
|
270
557
|
params: {}
|
|
271
558
|
})
|
|
272
559
|
});
|
|
560
|
+
const duration = Date.now() - startTime;
|
|
561
|
+
this.logDebug('Connection test response', {
|
|
562
|
+
status: response.status,
|
|
563
|
+
statusText: response.statusText,
|
|
564
|
+
duration
|
|
565
|
+
});
|
|
273
566
|
if (!response.ok) {
|
|
274
567
|
throw new Error(`Connection test failed: HTTP ${response.status}`);
|
|
275
568
|
}
|
|
@@ -277,11 +570,26 @@ class CelliumMCPClient {
|
|
|
277
570
|
if (result.error) {
|
|
278
571
|
throw new Error(`Connection test failed: ${result.error.message}`);
|
|
279
572
|
}
|
|
280
|
-
this.config.logger.debug('Connection test successful');
|
|
573
|
+
this.config.logger.debug({ duration }, 'Connection test successful');
|
|
281
574
|
}
|
|
282
575
|
async disconnect() {
|
|
283
576
|
this.config.logger.info('Disconnecting from Cellium MCP Server');
|
|
577
|
+
this.logDebug('Starting disconnect process', {
|
|
578
|
+
activeRequestCount: this.activeRequests.size,
|
|
579
|
+
transportConnected: this.transportState.connected
|
|
580
|
+
});
|
|
284
581
|
this.isConnected = false;
|
|
582
|
+
this.transportState.connected = false;
|
|
583
|
+
// Cancel any active requests
|
|
584
|
+
if (this.activeRequests.size > 0) {
|
|
585
|
+
this.config.logger.warn({
|
|
586
|
+
activeRequestCount: this.activeRequests.size
|
|
587
|
+
}, 'Cancelling active requests during disconnect');
|
|
588
|
+
for (const [id, timing] of this.activeRequests.entries()) {
|
|
589
|
+
this.logResponse(timing.method, id, false, undefined, 'Cancelled due to disconnect');
|
|
590
|
+
}
|
|
591
|
+
this.activeRequests.clear();
|
|
592
|
+
}
|
|
285
593
|
if (this.reconnectTimer) {
|
|
286
594
|
clearTimeout(this.reconnectTimer);
|
|
287
595
|
this.reconnectTimer = undefined;
|
|
@@ -290,8 +598,20 @@ class CelliumMCPClient {
|
|
|
290
598
|
clearInterval(this.keepAliveInterval);
|
|
291
599
|
this.keepAliveInterval = undefined;
|
|
292
600
|
}
|
|
293
|
-
|
|
294
|
-
|
|
601
|
+
try {
|
|
602
|
+
await this.localServer.close();
|
|
603
|
+
this.logDebug('Local MCP server closed successfully');
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
this.config.logger.error({
|
|
607
|
+
error: error instanceof Error ? error.message : error
|
|
608
|
+
}, 'Error closing local MCP server');
|
|
609
|
+
}
|
|
610
|
+
this.config.logger.info({
|
|
611
|
+
totalRequests: this.transportState.requestCount,
|
|
612
|
+
totalErrors: this.transportState.errorCount,
|
|
613
|
+
uptime: Date.now() - this.transportState.lastActivity
|
|
614
|
+
}, 'Disconnected successfully');
|
|
295
615
|
}
|
|
296
616
|
}
|
|
297
617
|
exports.CelliumMCPClient = CelliumMCPClient;
|