snow-flow 3.4.32 → 3.4.34
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.
|
@@ -67,11 +67,6 @@ export declare abstract class BaseMCPServer {
|
|
|
67
67
|
* Validate authentication with smart caching
|
|
68
68
|
*/
|
|
69
69
|
protected validateAuth(): Promise<AuthResult>;
|
|
70
|
-
/**
|
|
71
|
-
* 🔴 SNOW-003 FIX: Enhanced retry logic with intelligent backoff and circuit breaker
|
|
72
|
-
* Addresses the 19% failure rate with better retry strategies and failure prevention
|
|
73
|
-
*/
|
|
74
|
-
private executeWithRetry;
|
|
75
70
|
/**
|
|
76
71
|
* 🔴 SNOW-003 FIX: Calculate intelligent backoff based on error type and attempt
|
|
77
72
|
*/
|
|
@@ -17,6 +17,7 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
|
17
17
|
const servicenow_client_js_1 = require("../utils/servicenow-client.js");
|
|
18
18
|
const snow_oauth_js_1 = require("../utils/snow-oauth.js");
|
|
19
19
|
const logger_js_1 = require("../utils/logger.js");
|
|
20
|
+
const response_limiter_js_1 = require("./shared/response-limiter.js");
|
|
20
21
|
/**
|
|
21
22
|
* Base class for all ServiceNow MCP servers
|
|
22
23
|
* Provides common functionality to eliminate code duplication
|
|
@@ -78,7 +79,31 @@ class BaseMCPServer {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
// Execute tool with retry logic
|
|
81
|
-
|
|
82
|
+
let result = await this.executeWithRetry(name, args);
|
|
83
|
+
// Limit response size to prevent timeouts
|
|
84
|
+
const { limited, wasLimited, originalSize } = response_limiter_js_1.ResponseLimiter.limitResponse(result);
|
|
85
|
+
if (wasLimited) {
|
|
86
|
+
this.logger.warn(`Response limited for ${name}: ${originalSize} bytes -> ${JSON.stringify(limited).length} bytes`);
|
|
87
|
+
// Only create summary for EXTREMELY large responses (>2MB)
|
|
88
|
+
// Normal widgets/flows should pass through fine with 500KB limit
|
|
89
|
+
if (originalSize > 2000000) { // > 2MB - truly excessive
|
|
90
|
+
result = response_limiter_js_1.ResponseLimiter.createSummaryResponse(result, name);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result = limited;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Add token tracking metadata
|
|
97
|
+
const responseSize = JSON.stringify(result).length;
|
|
98
|
+
const estimatedTokens = Math.ceil(responseSize / 4);
|
|
99
|
+
if (result && typeof result === 'object') {
|
|
100
|
+
result._meta = {
|
|
101
|
+
...result._meta,
|
|
102
|
+
tokenCount: estimatedTokens,
|
|
103
|
+
responseSize,
|
|
104
|
+
wasLimited
|
|
105
|
+
};
|
|
106
|
+
}
|
|
82
107
|
// Update metrics
|
|
83
108
|
metrics.totalTime += Date.now() - startTime;
|
|
84
109
|
this.toolMetrics.set(name, metrics);
|
|
@@ -457,6 +482,38 @@ class BaseMCPServer {
|
|
|
457
482
|
getToolHandler(name) {
|
|
458
483
|
return this.toolHandlers.get(name);
|
|
459
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Execute tool with retry logic
|
|
487
|
+
*/
|
|
488
|
+
async executeWithRetry(name, args, maxRetries = 3) {
|
|
489
|
+
const handler = this.getToolHandler(name);
|
|
490
|
+
if (!handler) {
|
|
491
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Tool '${name}' not found`);
|
|
492
|
+
}
|
|
493
|
+
let lastError;
|
|
494
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
495
|
+
try {
|
|
496
|
+
// Execute the tool handler
|
|
497
|
+
const result = await handler(args);
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
lastError = error;
|
|
502
|
+
// Don't retry on non-retryable errors
|
|
503
|
+
if (error.code === types_js_1.ErrorCode.InvalidRequest ||
|
|
504
|
+
error.code === types_js_1.ErrorCode.MethodNotFound) {
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
// Log retry attempt
|
|
508
|
+
if (attempt < maxRetries) {
|
|
509
|
+
this.logger.warn(`Tool ${name} failed (attempt ${attempt}/${maxRetries}), retrying...`, error.message);
|
|
510
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(1000 * attempt, 5000)));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// All retries failed
|
|
515
|
+
throw lastError;
|
|
516
|
+
}
|
|
460
517
|
/**
|
|
461
518
|
* Graceful shutdown
|
|
462
519
|
*/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Response Limiter
|
|
3
|
+
* Prevents oversized responses that cause timeouts in Claude Code
|
|
4
|
+
*/
|
|
5
|
+
export declare class ResponseLimiter {
|
|
6
|
+
private static readonly MAX_RESPONSE_SIZE;
|
|
7
|
+
private static readonly MAX_ARRAY_ITEMS;
|
|
8
|
+
private static readonly MAX_TOKEN_ESTIMATE;
|
|
9
|
+
/**
|
|
10
|
+
* Limit response size to prevent timeouts
|
|
11
|
+
*/
|
|
12
|
+
static limitResponse(data: any): {
|
|
13
|
+
limited: any;
|
|
14
|
+
wasLimited: boolean;
|
|
15
|
+
originalSize?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Recursively limit object size
|
|
19
|
+
*/
|
|
20
|
+
private static limitObject;
|
|
21
|
+
/**
|
|
22
|
+
* Create a summary response when data is too large
|
|
23
|
+
*/
|
|
24
|
+
static createSummaryResponse(data: any, operation: string): any;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=response-limiter.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP Response Limiter
|
|
4
|
+
* Prevents oversized responses that cause timeouts in Claude Code
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ResponseLimiter = void 0;
|
|
8
|
+
class ResponseLimiter {
|
|
9
|
+
/**
|
|
10
|
+
* Limit response size to prevent timeouts
|
|
11
|
+
*/
|
|
12
|
+
static limitResponse(data) {
|
|
13
|
+
const originalString = JSON.stringify(data);
|
|
14
|
+
const originalSize = originalString.length;
|
|
15
|
+
// If response is small enough, return as-is
|
|
16
|
+
if (originalSize <= this.MAX_RESPONSE_SIZE) {
|
|
17
|
+
return { limited: data, wasLimited: false };
|
|
18
|
+
}
|
|
19
|
+
// Response too large - need to limit it
|
|
20
|
+
const limited = this.limitObject(data);
|
|
21
|
+
return {
|
|
22
|
+
limited,
|
|
23
|
+
wasLimited: true,
|
|
24
|
+
originalSize
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Recursively limit object size
|
|
29
|
+
*/
|
|
30
|
+
static limitObject(obj, depth = 0) {
|
|
31
|
+
// Don't go too deep
|
|
32
|
+
if (depth > 5) {
|
|
33
|
+
return '[DEPTH_LIMITED]';
|
|
34
|
+
}
|
|
35
|
+
// Handle null/undefined
|
|
36
|
+
if (obj === null || obj === undefined) {
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
// Handle primitives
|
|
40
|
+
if (typeof obj !== 'object') {
|
|
41
|
+
// Limit string length - 10KB per string field is reasonable
|
|
42
|
+
if (typeof obj === 'string' && obj.length > 10000) {
|
|
43
|
+
return obj.substring(0, 10000) + '... [TRUNCATED]';
|
|
44
|
+
}
|
|
45
|
+
return obj;
|
|
46
|
+
}
|
|
47
|
+
// Handle arrays
|
|
48
|
+
if (Array.isArray(obj)) {
|
|
49
|
+
if (obj.length > this.MAX_ARRAY_ITEMS) {
|
|
50
|
+
return [
|
|
51
|
+
...obj.slice(0, this.MAX_ARRAY_ITEMS).map(item => this.limitObject(item, depth + 1)),
|
|
52
|
+
`[... ${obj.length - this.MAX_ARRAY_ITEMS} more items]`
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
return obj.map(item => this.limitObject(item, depth + 1));
|
|
56
|
+
}
|
|
57
|
+
// Handle objects
|
|
58
|
+
const limited = {};
|
|
59
|
+
const keys = Object.keys(obj);
|
|
60
|
+
// Limit number of keys
|
|
61
|
+
const maxKeys = 50;
|
|
62
|
+
const keysToProcess = keys.slice(0, maxKeys);
|
|
63
|
+
for (const key of keysToProcess) {
|
|
64
|
+
limited[key] = this.limitObject(obj[key], depth + 1);
|
|
65
|
+
}
|
|
66
|
+
if (keys.length > maxKeys) {
|
|
67
|
+
limited._truncated = `${keys.length - maxKeys} more properties omitted`;
|
|
68
|
+
}
|
|
69
|
+
return limited;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a summary response when data is too large
|
|
73
|
+
*/
|
|
74
|
+
static createSummaryResponse(data, operation) {
|
|
75
|
+
const originalSize = JSON.stringify(data).length;
|
|
76
|
+
const estimatedTokens = Math.ceil(originalSize / 4);
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: `⚠️ Response too large (${estimatedTokens} tokens, ${originalSize} bytes)
|
|
81
|
+
|
|
82
|
+
Operation: ${operation}
|
|
83
|
+
Status: Success (data limited to prevent timeout)
|
|
84
|
+
|
|
85
|
+
Summary:
|
|
86
|
+
- Original size: ${(originalSize / 1024 / 1024).toFixed(2)}MB
|
|
87
|
+
- Token estimate: ${estimatedTokens}
|
|
88
|
+
- Response limit: ${(this.MAX_RESPONSE_SIZE / 1024).toFixed(0)}KB
|
|
89
|
+
|
|
90
|
+
💡 Tips to reduce response size:
|
|
91
|
+
1. Use specific field queries instead of '*'
|
|
92
|
+
2. Add pagination with smaller limits
|
|
93
|
+
3. Filter results more specifically
|
|
94
|
+
4. Use count operations instead of full data retrieval
|
|
95
|
+
|
|
96
|
+
Note: You can increase limits via environment variables:
|
|
97
|
+
- MCP_MAX_RESPONSE_SIZE (default: 500000 bytes)
|
|
98
|
+
- MCP_MAX_ARRAY_ITEMS (default: 500 items)`
|
|
99
|
+
}],
|
|
100
|
+
_meta: {
|
|
101
|
+
limited: true,
|
|
102
|
+
originalSize,
|
|
103
|
+
tokenEstimate: estimatedTokens
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.ResponseLimiter = ResponseLimiter;
|
|
109
|
+
// Configurable via environment variable, default to 500KB (reasonable for widgets/flows)
|
|
110
|
+
ResponseLimiter.MAX_RESPONSE_SIZE = parseInt(process.env.MCP_MAX_RESPONSE_SIZE || '500000'); // 500KB default
|
|
111
|
+
ResponseLimiter.MAX_ARRAY_ITEMS = parseInt(process.env.MCP_MAX_ARRAY_ITEMS || '500'); // 500 items default
|
|
112
|
+
ResponseLimiter.MAX_TOKEN_ESTIMATE = 125000; // ~500KB / 4 chars per token
|
|
113
|
+
//# sourceMappingURL=response-limiter.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-flow",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.34",
|
|
4
4
|
"description": "ServiceNow development framework with MCP server integration. Executes background scripts using ES5 JavaScript only (ServiceNow Rhino engine requirement). Provides 17 MCP servers for complete ServiceNow operations including widget deployment with coherence validation, table operations, script execution, machine learning, advanced features, and comprehensive platform management.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "commonjs",
|