cellium-mcp-client 2.0.8 → 2.0.10
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/client.d.ts +5 -0
- package/dist/client.js +185 -10
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export declare class CelliumMCPClient {
|
|
|
31
31
|
private setupServer;
|
|
32
32
|
private getSafeErrorResponse;
|
|
33
33
|
private makeHttpRequest;
|
|
34
|
+
private getErrorStatusCode;
|
|
35
|
+
private createClassifiedRequestError;
|
|
36
|
+
private extractBackendReason;
|
|
37
|
+
private classifyDenialCondition;
|
|
38
|
+
private getSafeDeniedMessage;
|
|
34
39
|
connect(): Promise<void>;
|
|
35
40
|
private setupTransportEventListeners;
|
|
36
41
|
serve(): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -307,6 +307,12 @@ class CelliumMCPClient {
|
|
|
307
307
|
}
|
|
308
308
|
getSafeErrorResponse(methodName, error) {
|
|
309
309
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
310
|
+
const classification = this.classifyDenialCondition({
|
|
311
|
+
statusCode: this.getErrorStatusCode(error),
|
|
312
|
+
message: errorMessage
|
|
313
|
+
});
|
|
314
|
+
const safeDeniedMessage = this.getSafeDeniedMessage(classification, errorMessage);
|
|
315
|
+
const responseErrorMessage = classification.isDenial ? safeDeniedMessage : errorMessage;
|
|
310
316
|
switch (methodName) {
|
|
311
317
|
case 'tools/list':
|
|
312
318
|
return { tools: [] };
|
|
@@ -316,7 +322,7 @@ class CelliumMCPClient {
|
|
|
316
322
|
return {
|
|
317
323
|
content: [{
|
|
318
324
|
type: 'text',
|
|
319
|
-
text: `Error calling tool: ${
|
|
325
|
+
text: `Error calling tool: ${safeDeniedMessage}`
|
|
320
326
|
}],
|
|
321
327
|
isError: true
|
|
322
328
|
};
|
|
@@ -325,13 +331,13 @@ class CelliumMCPClient {
|
|
|
325
331
|
contents: [{
|
|
326
332
|
uri: '',
|
|
327
333
|
mimeType: 'text/plain',
|
|
328
|
-
text: `Error reading resource: ${
|
|
334
|
+
text: `Error reading resource: ${safeDeniedMessage}`
|
|
329
335
|
}]
|
|
330
336
|
};
|
|
331
337
|
case 'ping':
|
|
332
338
|
return {};
|
|
333
339
|
default:
|
|
334
|
-
return { error:
|
|
340
|
+
return { error: responseErrorMessage };
|
|
335
341
|
}
|
|
336
342
|
}
|
|
337
343
|
async makeHttpRequest(method, params) {
|
|
@@ -409,7 +415,19 @@ class CelliumMCPClient {
|
|
|
409
415
|
duration
|
|
410
416
|
});
|
|
411
417
|
if (!response.ok) {
|
|
412
|
-
|
|
418
|
+
const classification = this.classifyDenialCondition({
|
|
419
|
+
statusCode: response.status,
|
|
420
|
+
message: response.statusText,
|
|
421
|
+
responseText
|
|
422
|
+
});
|
|
423
|
+
const backendReason = this.extractBackendReason(responseText);
|
|
424
|
+
const safeMessage = this.getSafeDeniedMessage({ ...classification, reason: backendReason ?? classification.reason }, `HTTP ${response.status}: ${response.statusText}`);
|
|
425
|
+
throw this.createClassifiedRequestError(safeMessage, {
|
|
426
|
+
statusCode: response.status,
|
|
427
|
+
denialCategory: classification.category,
|
|
428
|
+
denialReason: backendReason ?? classification.reason,
|
|
429
|
+
rawResponseSnippet: responseText.substring(0, 1000)
|
|
430
|
+
});
|
|
413
431
|
}
|
|
414
432
|
let jsonResponse;
|
|
415
433
|
try {
|
|
@@ -432,12 +450,30 @@ class CelliumMCPClient {
|
|
|
432
450
|
jsonrpcId: jsonResponse.id
|
|
433
451
|
});
|
|
434
452
|
if (jsonResponse.error) {
|
|
453
|
+
const classification = this.classifyDenialCondition({
|
|
454
|
+
statusCode: response.status,
|
|
455
|
+
message: jsonResponse.error?.message,
|
|
456
|
+
responseText,
|
|
457
|
+
remoteError: jsonResponse.error
|
|
458
|
+
});
|
|
459
|
+
const backendReason = this.extractBackendReason(jsonResponse.error) ?? this.extractBackendReason(responseText);
|
|
460
|
+
const safeMessage = this.getSafeDeniedMessage({ ...classification, reason: backendReason ?? classification.reason }, 'Remote server error');
|
|
435
461
|
this.config.logger.error({
|
|
436
462
|
requestId,
|
|
437
463
|
method,
|
|
438
|
-
|
|
464
|
+
statusCode: response.status,
|
|
465
|
+
serverError: jsonResponse.error,
|
|
466
|
+
denialCategory: classification.category,
|
|
467
|
+
denialReason: backendReason ?? classification.reason,
|
|
468
|
+
rawResponseSnippet: responseText.substring(0, 1000)
|
|
439
469
|
}, 'Remote server returned error');
|
|
440
|
-
throw
|
|
470
|
+
throw this.createClassifiedRequestError(safeMessage, {
|
|
471
|
+
statusCode: response.status,
|
|
472
|
+
denialCategory: classification.category,
|
|
473
|
+
denialReason: backendReason ?? classification.reason,
|
|
474
|
+
rawResponseSnippet: responseText.substring(0, 1000),
|
|
475
|
+
remoteError: jsonResponse.error
|
|
476
|
+
});
|
|
441
477
|
}
|
|
442
478
|
// Mark as connected on first successful response for http-stream endpoints
|
|
443
479
|
if (!this.isConnected && this.config.endpoint.includes('/http-stream')) {
|
|
@@ -449,6 +485,7 @@ class CelliumMCPClient {
|
|
|
449
485
|
}
|
|
450
486
|
catch (error) {
|
|
451
487
|
const duration = Date.now() - startTime;
|
|
488
|
+
const classifiedError = error;
|
|
452
489
|
this.config.logger.error({
|
|
453
490
|
error: error instanceof Error ? {
|
|
454
491
|
name: error.name,
|
|
@@ -458,12 +495,107 @@ class CelliumMCPClient {
|
|
|
458
495
|
requestId,
|
|
459
496
|
method,
|
|
460
497
|
duration,
|
|
461
|
-
endpoint: endpoint
|
|
498
|
+
endpoint: endpoint,
|
|
499
|
+
statusCode: classifiedError?.statusCode,
|
|
500
|
+
denialCategory: classifiedError?.denialCategory,
|
|
501
|
+
denialReason: classifiedError?.denialReason,
|
|
502
|
+
rawResponseSnippet: classifiedError?.rawResponseSnippet,
|
|
503
|
+
remoteError: classifiedError?.remoteError
|
|
462
504
|
}, 'HTTP request to remote server failed');
|
|
463
505
|
this.isConnected = false; // Mark as disconnected on error
|
|
464
506
|
throw error;
|
|
465
507
|
}
|
|
466
508
|
}
|
|
509
|
+
getErrorStatusCode(error) {
|
|
510
|
+
if (!error || typeof error !== 'object') {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
const possibleStatus = error.statusCode;
|
|
514
|
+
return typeof possibleStatus === 'number' ? possibleStatus : undefined;
|
|
515
|
+
}
|
|
516
|
+
createClassifiedRequestError(message, details) {
|
|
517
|
+
const classifiedError = new Error(message);
|
|
518
|
+
Object.assign(classifiedError, details);
|
|
519
|
+
return classifiedError;
|
|
520
|
+
}
|
|
521
|
+
extractBackendReason(input) {
|
|
522
|
+
if (!input) {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
const normalize = (value) => {
|
|
526
|
+
if (typeof value !== 'string') {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
const trimmed = value.trim();
|
|
530
|
+
return trimmed.length > 0 ? trimmed.substring(0, 300) : undefined;
|
|
531
|
+
};
|
|
532
|
+
if (typeof input === 'string') {
|
|
533
|
+
const direct = normalize(input);
|
|
534
|
+
if (direct) {
|
|
535
|
+
try {
|
|
536
|
+
const parsed = JSON.parse(input);
|
|
537
|
+
return normalize(parsed?.error?.message)
|
|
538
|
+
?? normalize(parsed?.error?.data?.reason)
|
|
539
|
+
?? normalize(parsed?.error?.data?.message)
|
|
540
|
+
?? normalize(parsed?.message)
|
|
541
|
+
?? normalize(parsed?.reason)
|
|
542
|
+
?? direct;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return direct;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
if (typeof input === 'object') {
|
|
551
|
+
const value = input;
|
|
552
|
+
return normalize(value?.error?.message)
|
|
553
|
+
?? normalize(value?.error?.data?.reason)
|
|
554
|
+
?? normalize(value?.error?.data?.message)
|
|
555
|
+
?? normalize(value?.message)
|
|
556
|
+
?? normalize(value?.reason)
|
|
557
|
+
?? normalize(value?.detail)
|
|
558
|
+
?? normalize(value?.error_description);
|
|
559
|
+
}
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
classifyDenialCondition(params) {
|
|
563
|
+
const backendReason = this.extractBackendReason(params.remoteError)
|
|
564
|
+
?? this.extractBackendReason(params.responseText)
|
|
565
|
+
?? this.extractBackendReason(params.message);
|
|
566
|
+
const text = `${params.message ?? ''} ${params.responseText ?? ''} ${backendReason ?? ''}`.toLowerCase();
|
|
567
|
+
const statusCode = params.statusCode;
|
|
568
|
+
const hasSessionPattern = /(session|concurrent|too many.*session|max.*session)/i.test(text);
|
|
569
|
+
const hasQuotaPattern = /(quota|rate.?limit|too many requests|credit|usage.*limit|limit exceeded)/i.test(text);
|
|
570
|
+
const hasPermissionPattern = /(forbidden|access denied|permission denied|insufficient permissions?|not authorized|not allowed|requires? (?:a )?(?:higher )?(?:plan|tier|subscription)|upgrade (?:your )?(?:plan|subscription)|subscription (?:required|inactive|expired)|plan (?:required|does not include)|tier (?:required|does not include))/i.test(text);
|
|
571
|
+
const hasAuthPattern = /(unauthorized|invalid token|expired token|authentication|auth failed|bearer)/i.test(text);
|
|
572
|
+
if (statusCode === 401 || hasAuthPattern) {
|
|
573
|
+
return { isDenial: true, category: 'auth', reason: backendReason, statusCode };
|
|
574
|
+
}
|
|
575
|
+
if (hasSessionPattern) {
|
|
576
|
+
return { isDenial: true, category: 'session_limit', reason: backendReason, statusCode };
|
|
577
|
+
}
|
|
578
|
+
if (statusCode === 429 || hasQuotaPattern) {
|
|
579
|
+
return { isDenial: true, category: 'quota', reason: backendReason, statusCode };
|
|
580
|
+
}
|
|
581
|
+
if (statusCode === 403 || hasPermissionPattern) {
|
|
582
|
+
return { isDenial: true, category: 'permission_tier', reason: backendReason, statusCode };
|
|
583
|
+
}
|
|
584
|
+
return { isDenial: false, reason: backendReason, statusCode };
|
|
585
|
+
}
|
|
586
|
+
getSafeDeniedMessage(classification, fallbackMessage) {
|
|
587
|
+
if (!classification.isDenial) {
|
|
588
|
+
return fallbackMessage;
|
|
589
|
+
}
|
|
590
|
+
const categoryLabel = {
|
|
591
|
+
auth: 'authentication failed',
|
|
592
|
+
session_limit: 'session limit reached',
|
|
593
|
+
permission_tier: 'permission or subscription tier restriction',
|
|
594
|
+
quota: 'quota or rate limit exceeded'
|
|
595
|
+
};
|
|
596
|
+
const prefix = categoryLabel[classification.category];
|
|
597
|
+
return `Request denied (${prefix})`;
|
|
598
|
+
}
|
|
467
599
|
async connect() {
|
|
468
600
|
try {
|
|
469
601
|
const transportMode = this.config.useStdio ? 'stdio' : 'http-streaming';
|
|
@@ -658,12 +790,55 @@ class CelliumMCPClient {
|
|
|
658
790
|
statusText: response.statusText,
|
|
659
791
|
duration
|
|
660
792
|
});
|
|
793
|
+
const responseText = await response.text();
|
|
661
794
|
if (!response.ok) {
|
|
662
|
-
|
|
795
|
+
const classification = this.classifyDenialCondition({
|
|
796
|
+
statusCode: response.status,
|
|
797
|
+
message: response.statusText,
|
|
798
|
+
responseText
|
|
799
|
+
});
|
|
800
|
+
const backendReason = this.extractBackendReason(responseText);
|
|
801
|
+
const safeMessage = this.getSafeDeniedMessage({ ...classification, reason: backendReason ?? classification.reason }, `HTTP ${response.status}: ${response.statusText}`);
|
|
802
|
+
this.config.logger.error({
|
|
803
|
+
statusCode: response.status,
|
|
804
|
+
statusText: response.statusText,
|
|
805
|
+
denialCategory: classification.category,
|
|
806
|
+
denialReason: backendReason ?? classification.reason,
|
|
807
|
+
rawResponseSnippet: responseText.substring(0, 1000)
|
|
808
|
+
}, 'Connection test received non-OK response');
|
|
809
|
+
throw new Error(`Connection test failed: ${safeMessage}`);
|
|
810
|
+
}
|
|
811
|
+
let result;
|
|
812
|
+
try {
|
|
813
|
+
result = JSON.parse(responseText);
|
|
814
|
+
}
|
|
815
|
+
catch (parseError) {
|
|
816
|
+
this.config.logger.error({
|
|
817
|
+
statusCode: response.status,
|
|
818
|
+
parseError: parseError instanceof Error ? parseError.message : parseError,
|
|
819
|
+
rawResponseSnippet: responseText.substring(0, 1000)
|
|
820
|
+
}, 'Connection test response was not valid JSON');
|
|
821
|
+
throw new Error('Connection test failed: Invalid JSON response from server');
|
|
663
822
|
}
|
|
664
|
-
const result = await response.json();
|
|
665
823
|
if (result.error) {
|
|
666
|
-
|
|
824
|
+
const backendReason = this.extractBackendReason(result.error) ?? this.extractBackendReason(responseText);
|
|
825
|
+
const rawErrorMessage = backendReason
|
|
826
|
+
?? (typeof result.error?.message === 'string' ? result.error.message : 'Remote server error');
|
|
827
|
+
const classification = this.classifyDenialCondition({
|
|
828
|
+
statusCode: response.status,
|
|
829
|
+
message: rawErrorMessage,
|
|
830
|
+
responseText,
|
|
831
|
+
remoteError: result.error
|
|
832
|
+
});
|
|
833
|
+
const safeMessage = this.getSafeDeniedMessage({ ...classification, reason: backendReason ?? classification.reason }, 'Remote server returned an error during startup');
|
|
834
|
+
this.config.logger.error({
|
|
835
|
+
statusCode: response.status,
|
|
836
|
+
denialCategory: classification.category,
|
|
837
|
+
denialReason: backendReason ?? classification.reason,
|
|
838
|
+
remoteError: result.error,
|
|
839
|
+
rawResponseSnippet: responseText.substring(0, 1000)
|
|
840
|
+
}, 'Connection test returned JSON-RPC error');
|
|
841
|
+
throw new Error(`Connection test failed: ${safeMessage}`);
|
|
667
842
|
}
|
|
668
843
|
this.config.logger.debug({ duration }, 'Connection test successful');
|
|
669
844
|
}
|