@contextrail/code-review-agent 0.1.1-alpha.0

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.
Files changed (84) hide show
  1. package/LICENSE +26 -0
  2. package/MODEL_RECOMMENDATIONS.md +178 -0
  3. package/README.md +177 -0
  4. package/dist/config/defaults.d.ts +72 -0
  5. package/dist/config/defaults.js +113 -0
  6. package/dist/config/index.d.ts +34 -0
  7. package/dist/config/index.js +89 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +603 -0
  10. package/dist/llm/factory.d.ts +21 -0
  11. package/dist/llm/factory.js +50 -0
  12. package/dist/llm/index.d.ts +3 -0
  13. package/dist/llm/index.js +2 -0
  14. package/dist/llm/service.d.ts +38 -0
  15. package/dist/llm/service.js +191 -0
  16. package/dist/llm/types.d.ts +119 -0
  17. package/dist/llm/types.js +1 -0
  18. package/dist/logging/logger.d.ts +9 -0
  19. package/dist/logging/logger.js +52 -0
  20. package/dist/mcp/client.d.ts +429 -0
  21. package/dist/mcp/client.js +173 -0
  22. package/dist/mcp/mcp-tools.d.ts +292 -0
  23. package/dist/mcp/mcp-tools.js +40 -0
  24. package/dist/mcp/token-validation.d.ts +31 -0
  25. package/dist/mcp/token-validation.js +57 -0
  26. package/dist/mcp/tools-provider.d.ts +18 -0
  27. package/dist/mcp/tools-provider.js +24 -0
  28. package/dist/observability/index.d.ts +2 -0
  29. package/dist/observability/index.js +1 -0
  30. package/dist/observability/metrics.d.ts +48 -0
  31. package/dist/observability/metrics.js +86 -0
  32. package/dist/orchestrator/agentic-orchestrator.d.ts +29 -0
  33. package/dist/orchestrator/agentic-orchestrator.js +136 -0
  34. package/dist/orchestrator/prompts.d.ts +25 -0
  35. package/dist/orchestrator/prompts.js +98 -0
  36. package/dist/orchestrator/validation.d.ts +2 -0
  37. package/dist/orchestrator/validation.js +7 -0
  38. package/dist/orchestrator/writer.d.ts +4 -0
  39. package/dist/orchestrator/writer.js +17 -0
  40. package/dist/output/aggregator.d.ts +30 -0
  41. package/dist/output/aggregator.js +132 -0
  42. package/dist/output/prompts.d.ts +32 -0
  43. package/dist/output/prompts.js +153 -0
  44. package/dist/output/schema.d.ts +1515 -0
  45. package/dist/output/schema.js +224 -0
  46. package/dist/output/writer.d.ts +31 -0
  47. package/dist/output/writer.js +120 -0
  48. package/dist/review-inputs/chunking.d.ts +29 -0
  49. package/dist/review-inputs/chunking.js +113 -0
  50. package/dist/review-inputs/diff-summary.d.ts +52 -0
  51. package/dist/review-inputs/diff-summary.js +83 -0
  52. package/dist/review-inputs/file-patterns.d.ts +40 -0
  53. package/dist/review-inputs/file-patterns.js +182 -0
  54. package/dist/review-inputs/filtering.d.ts +31 -0
  55. package/dist/review-inputs/filtering.js +53 -0
  56. package/dist/review-inputs/git-diff-provider.d.ts +2 -0
  57. package/dist/review-inputs/git-diff-provider.js +42 -0
  58. package/dist/review-inputs/index.d.ts +46 -0
  59. package/dist/review-inputs/index.js +122 -0
  60. package/dist/review-inputs/path-validation.d.ts +10 -0
  61. package/dist/review-inputs/path-validation.js +37 -0
  62. package/dist/review-inputs/surrounding-context.d.ts +35 -0
  63. package/dist/review-inputs/surrounding-context.js +180 -0
  64. package/dist/review-inputs/triage.d.ts +57 -0
  65. package/dist/review-inputs/triage.js +81 -0
  66. package/dist/reviewers/executor.d.ts +41 -0
  67. package/dist/reviewers/executor.js +357 -0
  68. package/dist/reviewers/findings-merge.d.ts +9 -0
  69. package/dist/reviewers/findings-merge.js +131 -0
  70. package/dist/reviewers/iteration.d.ts +17 -0
  71. package/dist/reviewers/iteration.js +95 -0
  72. package/dist/reviewers/persistence.d.ts +17 -0
  73. package/dist/reviewers/persistence.js +55 -0
  74. package/dist/reviewers/progress-tracker.d.ts +115 -0
  75. package/dist/reviewers/progress-tracker.js +194 -0
  76. package/dist/reviewers/prompt.d.ts +42 -0
  77. package/dist/reviewers/prompt.js +246 -0
  78. package/dist/reviewers/tool-call-tracker.d.ts +18 -0
  79. package/dist/reviewers/tool-call-tracker.js +40 -0
  80. package/dist/reviewers/types.d.ts +12 -0
  81. package/dist/reviewers/types.js +1 -0
  82. package/dist/reviewers/validation-rules.d.ts +27 -0
  83. package/dist/reviewers/validation-rules.js +189 -0
  84. package/package.json +79 -0
@@ -0,0 +1,429 @@
1
+ import type { Logger } from '../logging/logger.js';
2
+ export type McpClientConfig = {
3
+ serverUrl: string;
4
+ authToken?: string;
5
+ clientName?: string;
6
+ clientVersion?: string;
7
+ logger?: Logger;
8
+ logContext?: {
9
+ requestId?: string;
10
+ userId?: string;
11
+ correlationId?: string;
12
+ };
13
+ };
14
+ export declare class McpClient {
15
+ private static readonly DEFAULT_MAX_FAILURES;
16
+ private static readonly DEFAULT_MAX_RETRIES;
17
+ private static readonly DEFAULT_BASE_RETRY_DELAY_MS;
18
+ private client;
19
+ private transport?;
20
+ private readonly serverUrl;
21
+ private readonly authToken?;
22
+ private readonly logger?;
23
+ private readonly logContext;
24
+ private failureCount;
25
+ private circuitOpenUntil?;
26
+ private readonly maxFailures;
27
+ private readonly maxRetries;
28
+ private readonly baseRetryDelayMs;
29
+ constructor(config: McpClientConfig);
30
+ connect(): Promise<void>;
31
+ close(): Promise<void>;
32
+ listPrompts(): Promise<{
33
+ [x: string]: unknown;
34
+ prompts: {
35
+ name: string;
36
+ description?: string | undefined;
37
+ arguments?: {
38
+ name: string;
39
+ description?: string | undefined;
40
+ required?: boolean | undefined;
41
+ }[] | undefined;
42
+ _meta?: {
43
+ [x: string]: unknown;
44
+ } | undefined;
45
+ icons?: {
46
+ src: string;
47
+ mimeType?: string | undefined;
48
+ sizes?: string[] | undefined;
49
+ }[] | undefined;
50
+ title?: string | undefined;
51
+ }[];
52
+ _meta?: {
53
+ [x: string]: unknown;
54
+ "io.modelcontextprotocol/related-task"?: {
55
+ [x: string]: unknown;
56
+ taskId: string;
57
+ } | undefined;
58
+ } | undefined;
59
+ nextCursor?: string | undefined;
60
+ }>;
61
+ getPrompt(params: {
62
+ name: string;
63
+ arguments?: Record<string, string>;
64
+ }): Promise<{
65
+ [x: string]: unknown;
66
+ messages: {
67
+ role: "user" | "assistant";
68
+ content: {
69
+ type: "text";
70
+ text: string;
71
+ annotations?: {
72
+ audience?: ("user" | "assistant")[] | undefined;
73
+ priority?: number | undefined;
74
+ lastModified?: string | undefined;
75
+ } | undefined;
76
+ _meta?: Record<string, unknown> | undefined;
77
+ } | {
78
+ type: "image";
79
+ data: string;
80
+ mimeType: string;
81
+ annotations?: {
82
+ audience?: ("user" | "assistant")[] | undefined;
83
+ priority?: number | undefined;
84
+ lastModified?: string | undefined;
85
+ } | undefined;
86
+ _meta?: Record<string, unknown> | undefined;
87
+ } | {
88
+ type: "audio";
89
+ data: string;
90
+ mimeType: string;
91
+ annotations?: {
92
+ audience?: ("user" | "assistant")[] | undefined;
93
+ priority?: number | undefined;
94
+ lastModified?: string | undefined;
95
+ } | undefined;
96
+ _meta?: Record<string, unknown> | undefined;
97
+ } | {
98
+ type: "resource";
99
+ resource: {
100
+ uri: string;
101
+ text: string;
102
+ mimeType?: string | undefined;
103
+ _meta?: Record<string, unknown> | undefined;
104
+ } | {
105
+ uri: string;
106
+ blob: string;
107
+ mimeType?: string | undefined;
108
+ _meta?: Record<string, unknown> | undefined;
109
+ };
110
+ annotations?: {
111
+ audience?: ("user" | "assistant")[] | undefined;
112
+ priority?: number | undefined;
113
+ lastModified?: string | undefined;
114
+ } | undefined;
115
+ _meta?: Record<string, unknown> | undefined;
116
+ } | {
117
+ uri: string;
118
+ name: string;
119
+ type: "resource_link";
120
+ description?: string | undefined;
121
+ mimeType?: string | undefined;
122
+ annotations?: {
123
+ audience?: ("user" | "assistant")[] | undefined;
124
+ priority?: number | undefined;
125
+ lastModified?: string | undefined;
126
+ } | undefined;
127
+ _meta?: {
128
+ [x: string]: unknown;
129
+ } | undefined;
130
+ icons?: {
131
+ src: string;
132
+ mimeType?: string | undefined;
133
+ sizes?: string[] | undefined;
134
+ }[] | undefined;
135
+ title?: string | undefined;
136
+ };
137
+ }[];
138
+ _meta?: {
139
+ [x: string]: unknown;
140
+ "io.modelcontextprotocol/related-task"?: {
141
+ [x: string]: unknown;
142
+ taskId: string;
143
+ } | undefined;
144
+ } | undefined;
145
+ description?: string | undefined;
146
+ }>;
147
+ searchContexts(params: Record<string, unknown>): Promise<{
148
+ [x: string]: unknown;
149
+ content: ({
150
+ type: "text";
151
+ text: string;
152
+ annotations?: {
153
+ audience?: ("user" | "assistant")[] | undefined;
154
+ priority?: number | undefined;
155
+ lastModified?: string | undefined;
156
+ } | undefined;
157
+ _meta?: Record<string, unknown> | undefined;
158
+ } | {
159
+ type: "image";
160
+ data: string;
161
+ mimeType: string;
162
+ annotations?: {
163
+ audience?: ("user" | "assistant")[] | undefined;
164
+ priority?: number | undefined;
165
+ lastModified?: string | undefined;
166
+ } | undefined;
167
+ _meta?: Record<string, unknown> | undefined;
168
+ } | {
169
+ type: "audio";
170
+ data: string;
171
+ mimeType: string;
172
+ annotations?: {
173
+ audience?: ("user" | "assistant")[] | undefined;
174
+ priority?: number | undefined;
175
+ lastModified?: string | undefined;
176
+ } | undefined;
177
+ _meta?: Record<string, unknown> | undefined;
178
+ } | {
179
+ type: "resource";
180
+ resource: {
181
+ uri: string;
182
+ text: string;
183
+ mimeType?: string | undefined;
184
+ _meta?: Record<string, unknown> | undefined;
185
+ } | {
186
+ uri: string;
187
+ blob: string;
188
+ mimeType?: string | undefined;
189
+ _meta?: Record<string, unknown> | undefined;
190
+ };
191
+ annotations?: {
192
+ audience?: ("user" | "assistant")[] | undefined;
193
+ priority?: number | undefined;
194
+ lastModified?: string | undefined;
195
+ } | undefined;
196
+ _meta?: Record<string, unknown> | undefined;
197
+ } | {
198
+ uri: string;
199
+ name: string;
200
+ type: "resource_link";
201
+ description?: string | undefined;
202
+ mimeType?: string | undefined;
203
+ annotations?: {
204
+ audience?: ("user" | "assistant")[] | undefined;
205
+ priority?: number | undefined;
206
+ lastModified?: string | undefined;
207
+ } | undefined;
208
+ _meta?: {
209
+ [x: string]: unknown;
210
+ } | undefined;
211
+ icons?: {
212
+ src: string;
213
+ mimeType?: string | undefined;
214
+ sizes?: string[] | undefined;
215
+ }[] | undefined;
216
+ title?: string | undefined;
217
+ })[];
218
+ _meta?: {
219
+ [x: string]: unknown;
220
+ "io.modelcontextprotocol/related-task"?: {
221
+ [x: string]: unknown;
222
+ taskId: string;
223
+ } | undefined;
224
+ } | undefined;
225
+ structuredContent?: Record<string, unknown> | undefined;
226
+ isError?: boolean | undefined;
227
+ } | {
228
+ [x: string]: unknown;
229
+ toolResult: unknown;
230
+ _meta?: {
231
+ [x: string]: unknown;
232
+ "io.modelcontextprotocol/related-task"?: {
233
+ [x: string]: unknown;
234
+ taskId: string;
235
+ } | undefined;
236
+ } | undefined;
237
+ }>;
238
+ getContext(params: Record<string, unknown>): Promise<{
239
+ [x: string]: unknown;
240
+ content: ({
241
+ type: "text";
242
+ text: string;
243
+ annotations?: {
244
+ audience?: ("user" | "assistant")[] | undefined;
245
+ priority?: number | undefined;
246
+ lastModified?: string | undefined;
247
+ } | undefined;
248
+ _meta?: Record<string, unknown> | undefined;
249
+ } | {
250
+ type: "image";
251
+ data: string;
252
+ mimeType: string;
253
+ annotations?: {
254
+ audience?: ("user" | "assistant")[] | undefined;
255
+ priority?: number | undefined;
256
+ lastModified?: string | undefined;
257
+ } | undefined;
258
+ _meta?: Record<string, unknown> | undefined;
259
+ } | {
260
+ type: "audio";
261
+ data: string;
262
+ mimeType: string;
263
+ annotations?: {
264
+ audience?: ("user" | "assistant")[] | undefined;
265
+ priority?: number | undefined;
266
+ lastModified?: string | undefined;
267
+ } | undefined;
268
+ _meta?: Record<string, unknown> | undefined;
269
+ } | {
270
+ type: "resource";
271
+ resource: {
272
+ uri: string;
273
+ text: string;
274
+ mimeType?: string | undefined;
275
+ _meta?: Record<string, unknown> | undefined;
276
+ } | {
277
+ uri: string;
278
+ blob: string;
279
+ mimeType?: string | undefined;
280
+ _meta?: Record<string, unknown> | undefined;
281
+ };
282
+ annotations?: {
283
+ audience?: ("user" | "assistant")[] | undefined;
284
+ priority?: number | undefined;
285
+ lastModified?: string | undefined;
286
+ } | undefined;
287
+ _meta?: Record<string, unknown> | undefined;
288
+ } | {
289
+ uri: string;
290
+ name: string;
291
+ type: "resource_link";
292
+ description?: string | undefined;
293
+ mimeType?: string | undefined;
294
+ annotations?: {
295
+ audience?: ("user" | "assistant")[] | undefined;
296
+ priority?: number | undefined;
297
+ lastModified?: string | undefined;
298
+ } | undefined;
299
+ _meta?: {
300
+ [x: string]: unknown;
301
+ } | undefined;
302
+ icons?: {
303
+ src: string;
304
+ mimeType?: string | undefined;
305
+ sizes?: string[] | undefined;
306
+ }[] | undefined;
307
+ title?: string | undefined;
308
+ })[];
309
+ _meta?: {
310
+ [x: string]: unknown;
311
+ "io.modelcontextprotocol/related-task"?: {
312
+ [x: string]: unknown;
313
+ taskId: string;
314
+ } | undefined;
315
+ } | undefined;
316
+ structuredContent?: Record<string, unknown> | undefined;
317
+ isError?: boolean | undefined;
318
+ } | {
319
+ [x: string]: unknown;
320
+ toolResult: unknown;
321
+ _meta?: {
322
+ [x: string]: unknown;
323
+ "io.modelcontextprotocol/related-task"?: {
324
+ [x: string]: unknown;
325
+ taskId: string;
326
+ } | undefined;
327
+ } | undefined;
328
+ }>;
329
+ resolveDependencies(params: Record<string, unknown>): Promise<{
330
+ [x: string]: unknown;
331
+ content: ({
332
+ type: "text";
333
+ text: string;
334
+ annotations?: {
335
+ audience?: ("user" | "assistant")[] | undefined;
336
+ priority?: number | undefined;
337
+ lastModified?: string | undefined;
338
+ } | undefined;
339
+ _meta?: Record<string, unknown> | undefined;
340
+ } | {
341
+ type: "image";
342
+ data: string;
343
+ mimeType: string;
344
+ annotations?: {
345
+ audience?: ("user" | "assistant")[] | undefined;
346
+ priority?: number | undefined;
347
+ lastModified?: string | undefined;
348
+ } | undefined;
349
+ _meta?: Record<string, unknown> | undefined;
350
+ } | {
351
+ type: "audio";
352
+ data: string;
353
+ mimeType: string;
354
+ annotations?: {
355
+ audience?: ("user" | "assistant")[] | undefined;
356
+ priority?: number | undefined;
357
+ lastModified?: string | undefined;
358
+ } | undefined;
359
+ _meta?: Record<string, unknown> | undefined;
360
+ } | {
361
+ type: "resource";
362
+ resource: {
363
+ uri: string;
364
+ text: string;
365
+ mimeType?: string | undefined;
366
+ _meta?: Record<string, unknown> | undefined;
367
+ } | {
368
+ uri: string;
369
+ blob: string;
370
+ mimeType?: string | undefined;
371
+ _meta?: Record<string, unknown> | undefined;
372
+ };
373
+ annotations?: {
374
+ audience?: ("user" | "assistant")[] | undefined;
375
+ priority?: number | undefined;
376
+ lastModified?: string | undefined;
377
+ } | undefined;
378
+ _meta?: Record<string, unknown> | undefined;
379
+ } | {
380
+ uri: string;
381
+ name: string;
382
+ type: "resource_link";
383
+ description?: string | undefined;
384
+ mimeType?: string | undefined;
385
+ annotations?: {
386
+ audience?: ("user" | "assistant")[] | undefined;
387
+ priority?: number | undefined;
388
+ lastModified?: string | undefined;
389
+ } | undefined;
390
+ _meta?: {
391
+ [x: string]: unknown;
392
+ } | undefined;
393
+ icons?: {
394
+ src: string;
395
+ mimeType?: string | undefined;
396
+ sizes?: string[] | undefined;
397
+ }[] | undefined;
398
+ title?: string | undefined;
399
+ })[];
400
+ _meta?: {
401
+ [x: string]: unknown;
402
+ "io.modelcontextprotocol/related-task"?: {
403
+ [x: string]: unknown;
404
+ taskId: string;
405
+ } | undefined;
406
+ } | undefined;
407
+ structuredContent?: Record<string, unknown> | undefined;
408
+ isError?: boolean | undefined;
409
+ } | {
410
+ [x: string]: unknown;
411
+ toolResult: unknown;
412
+ _meta?: {
413
+ [x: string]: unknown;
414
+ "io.modelcontextprotocol/related-task"?: {
415
+ [x: string]: unknown;
416
+ taskId: string;
417
+ } | undefined;
418
+ } | undefined;
419
+ }>;
420
+ private executeWithRetry;
421
+ private ensureCircuitClosed;
422
+ private recordSuccess;
423
+ private recordFailure;
424
+ private isErrorWithCode;
425
+ private isTransientError;
426
+ private wrapError;
427
+ private logError;
428
+ private wait;
429
+ }
@@ -0,0 +1,173 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport, } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ export class McpClient {
4
+ static DEFAULT_MAX_FAILURES = 3;
5
+ static DEFAULT_MAX_RETRIES = 3;
6
+ static DEFAULT_BASE_RETRY_DELAY_MS = 200;
7
+ client;
8
+ transport;
9
+ serverUrl;
10
+ authToken;
11
+ logger;
12
+ logContext;
13
+ failureCount = 0;
14
+ circuitOpenUntil;
15
+ maxFailures = McpClient.DEFAULT_MAX_FAILURES;
16
+ maxRetries = McpClient.DEFAULT_MAX_RETRIES;
17
+ baseRetryDelayMs = McpClient.DEFAULT_BASE_RETRY_DELAY_MS;
18
+ constructor(config) {
19
+ this.serverUrl = config.serverUrl;
20
+ this.authToken = config.authToken;
21
+ this.logger = config.logger;
22
+ this.logContext = config.logContext;
23
+ this.client = new Client({
24
+ name: config.clientName ?? 'code-review-agent',
25
+ version: config.clientVersion ?? '0.1.0',
26
+ });
27
+ }
28
+ async connect() {
29
+ if (this.transport) {
30
+ return;
31
+ }
32
+ // Validate token before connecting (client-side check)
33
+ if (this.authToken) {
34
+ const { validateToken } = await import('./token-validation.js');
35
+ const validation = validateToken(this.authToken);
36
+ if (!validation.valid) {
37
+ this.logger?.warn(`Token validation warning: ${validation.reason}. Proceeding with connection (server will perform full validation).`);
38
+ }
39
+ }
40
+ this.logger?.debug('Connecting to MCP server:', this.serverUrl);
41
+ const transportOptions = this.authToken
42
+ ? { requestInit: { headers: { Authorization: `Bearer ${this.authToken}` } } }
43
+ : undefined;
44
+ this.transport = new StreamableHTTPClientTransport(new URL(this.serverUrl), transportOptions);
45
+ try {
46
+ await this.client.connect(this.transport);
47
+ this.logger?.debug('Connected to MCP server:', this.serverUrl);
48
+ }
49
+ catch (error) {
50
+ this.transport = undefined;
51
+ const message = error instanceof Error ? error.message : 'Unknown error';
52
+ throw new Error(`MCP connection failed for ${this.serverUrl}: ${message}`);
53
+ }
54
+ }
55
+ async close() {
56
+ this.logger?.debug('Closing MCP client:', this.serverUrl);
57
+ await this.client.close();
58
+ this.transport = undefined;
59
+ }
60
+ async listPrompts() {
61
+ this.logger?.debug('Listing prompts');
62
+ return this.executeWithRetry('listPrompts', () => this.client.listPrompts());
63
+ }
64
+ async getPrompt(params) {
65
+ this.logger?.debug('Getting prompt:', params);
66
+ return this.executeWithRetry('getPrompt', () => this.client.getPrompt(params));
67
+ }
68
+ async searchContexts(params) {
69
+ this.logger?.debug('Searching contexts:', params);
70
+ return this.executeWithRetry('search_contexts', () => this.client.callTool({
71
+ name: 'search_contexts',
72
+ arguments: params,
73
+ }));
74
+ }
75
+ async getContext(params) {
76
+ this.logger?.debug('Getting context:', params);
77
+ return this.executeWithRetry('get_context', () => this.client.callTool({
78
+ name: 'get_context',
79
+ arguments: params,
80
+ }));
81
+ }
82
+ async resolveDependencies(params) {
83
+ this.logger?.debug('Resolving dependencies:', params);
84
+ return this.executeWithRetry('resolve_dependencies', () => this.client.callTool({
85
+ name: 'resolve_dependencies',
86
+ arguments: params,
87
+ }));
88
+ }
89
+ async executeWithRetry(operation, fn) {
90
+ this.ensureCircuitClosed(operation);
91
+ for (let attempt = 1; attempt <= this.maxRetries; attempt += 1) {
92
+ this.logger?.debug(`MCP ${operation} attempt ${attempt}/${this.maxRetries}`);
93
+ try {
94
+ const result = await fn();
95
+ this.recordSuccess();
96
+ return result;
97
+ }
98
+ catch (error) {
99
+ const wrapped = this.wrapError(operation, error);
100
+ this.recordFailure();
101
+ this.logError(operation, wrapped, attempt);
102
+ if (!this.isTransientError(error) || attempt === this.maxRetries) {
103
+ throw wrapped;
104
+ }
105
+ await this.wait(this.baseRetryDelayMs * 2 ** (attempt - 1));
106
+ }
107
+ }
108
+ throw new Error(`MCP ${operation} failed after ${this.maxRetries} attempts`);
109
+ }
110
+ ensureCircuitClosed(operation) {
111
+ if (this.circuitOpenUntil && Date.now() < this.circuitOpenUntil) {
112
+ throw new Error(`Circuit breaker open for ${operation} (${this.serverUrl})`);
113
+ }
114
+ }
115
+ recordSuccess() {
116
+ this.failureCount = 0;
117
+ this.circuitOpenUntil = undefined;
118
+ }
119
+ recordFailure() {
120
+ this.failureCount += 1;
121
+ if (this.failureCount >= this.maxFailures) {
122
+ this.circuitOpenUntil = Date.now() + this.baseRetryDelayMs * 10;
123
+ }
124
+ }
125
+ isErrorWithCode(error) {
126
+ if (!error || typeof error !== 'object') {
127
+ return false;
128
+ }
129
+ return 'code' in error && typeof error.code === 'string';
130
+ }
131
+ isTransientError(error) {
132
+ if (this.isErrorWithCode(error)) {
133
+ return ['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'ECONNREFUSED', 'ENETUNREACH'].includes(error.code);
134
+ }
135
+ if (error instanceof Error) {
136
+ return /timeout|timed out|ECONNRESET|ETIMEDOUT/i.test(error.message);
137
+ }
138
+ return false;
139
+ }
140
+ wrapError(operation, error) {
141
+ if (error instanceof Error) {
142
+ if (process.env.NODE_ENV === 'development' && error.stack) {
143
+ return new Error(`MCP ${operation} failed for ${this.serverUrl}: ${error.message}\n${error.stack}`);
144
+ }
145
+ return new Error(`MCP ${operation} failed for ${this.serverUrl}: ${error.message}`);
146
+ }
147
+ return new Error(`MCP ${operation} failed for ${this.serverUrl}: Unknown error`);
148
+ }
149
+ logError(operation, error, attempt) {
150
+ const timestamp = new Date().toISOString();
151
+ const payload = {
152
+ timestamp,
153
+ level: 'error',
154
+ message: `MCP ${operation} error`,
155
+ operation,
156
+ serverUrl: this.serverUrl,
157
+ attempt,
158
+ requestId: this.logContext?.requestId ?? null,
159
+ userId: this.logContext?.userId ?? null,
160
+ correlationId: this.logContext?.correlationId ?? null,
161
+ errorMessage: error.message,
162
+ errorStack: process.env.NODE_ENV === 'development' ? (error.stack ?? null) : null,
163
+ };
164
+ if (this.logger) {
165
+ this.logger.error(payload);
166
+ return;
167
+ }
168
+ console.error(JSON.stringify(payload));
169
+ }
170
+ wait(durationMs) {
171
+ return new Promise((resolve) => setTimeout(resolve, durationMs));
172
+ }
173
+ }