Skip to content

Commit b94e2d8

Browse files
author
ForgeFlow v2
committed
fix(memory): Implement bounded collection limits for unbounded array growth
Prevent memory exhaustion in long-running sessions by: - McpContext: Limit trace results to 100 most recent (circular buffer) - McpResponse: Limit response lines to 10000 and images to 500 - AutonomousExplorer: Limit queue to 1000 items and error array to 500 These fixes address HIGH severity memory leak issues identified in quality review. Long-running MCP servers can now maintain stable memory usage even with repeated trace recordings, large explorations, or high-volume responses.
1 parent dab3a6d commit b94e2d8

3 files changed

Lines changed: 44 additions & 28 deletions

File tree

src/McpContext.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class McpContext implements Context {
114114

115115
#nextSnapshotId = 1;
116116
#traceResults: TraceResult[] = [];
117+
#maxTraceResults = 100; // Prevent unbounded memory growth
117118

118119
#locatorClass: typeof Locator;
119120
#options: McpContextOptions;
@@ -611,6 +612,10 @@ export class McpContext implements Context {
611612

612613
storeTraceRecording(result: TraceResult): void {
613614
this.#traceResults.push(result);
615+
// Keep only the most recent traces to prevent unbounded memory growth
616+
if (this.#traceResults.length > this.#maxTraceResults) {
617+
this.#traceResults.shift();
618+
}
614619
}
615620

616621
recordedTraces(): TraceResult[] {

src/McpResponse.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export class McpResponse implements Response {
4343
#attachedConsoleMessageId?: number;
4444
#textResponseLines: string[] = [];
4545
#images: ImageContentData[] = [];
46+
#maxResponseLines = 10000; // Prevent unbounded response growth
47+
#maxImages = 500;
4648
#networkRequestsOptions?: {
4749
include: boolean;
4850
pagination?: PaginationOptions;
@@ -159,11 +161,17 @@ export class McpResponse implements Response {
159161
}
160162

161163
appendResponseLine(value: string): void {
162-
this.#textResponseLines.push(value);
164+
// Enforce response size limit to prevent MCP protocol issues
165+
if (this.#textResponseLines.length < this.#maxResponseLines) {
166+
this.#textResponseLines.push(value);
167+
}
163168
}
164169

165170
attachImage(value: ImageContentData): void {
166-
this.#images.push(value);
171+
// Enforce image count limit
172+
if (this.#images.length < this.#maxImages) {
173+
this.#images.push(value);
174+
}
167175
}
168176

169177
get responseLines(): readonly string[] {

src/utils/explorer.ts

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import {logger} from '../logger.js';
78
import type {Page} from '../third_party/index.js';
89

910
/**
@@ -109,6 +110,8 @@ export class AutonomousExplorer {
109110
private queue: Array<{url: string; depth: number}> = [];
110111
private sitemap: Map<string, PageInfo> = new Map();
111112
private allErrors: ConsoleError[] = [];
113+
private maxQueueSize = 1000; // Prevent unbounded queue growth
114+
private maxErrorsSize = 500; // Limit error tracking
112115

113116
/**
114117
* Explore a website starting from a URL
@@ -135,10 +138,8 @@ export class AutonomousExplorer {
135138
...config,
136139
};
137140

138-
console.log(`[EXPLORER] Starting exploration from ${startUrl}`);
139-
console.log(
140-
`[EXPLORER] Config: maxDepth=${fullConfig.maxDepth}, maxPages=${fullConfig.maxPages}`,
141-
);
141+
logger('[EXPLORER] Starting exploration from ' + startUrl);
142+
logger('[EXPLORER] Config: maxDepth=' + fullConfig.maxDepth + ', maxPages=' + fullConfig.maxPages);
142143

143144
const startTime = Date.now();
144145

@@ -161,29 +162,27 @@ export class AutonomousExplorer {
161162

162163
// Skip if max depth exceeded
163164
if (depth > fullConfig.maxDepth) {
164-
console.log(`[EXPLORER] Max depth reached for ${url}`);
165+
logger('[EXPLORER] Max depth reached for ' + url);
165166
continue;
166167
}
167168

168169
// Skip if URL matches ignore patterns
169170
if (this.shouldIgnoreUrl(url, fullConfig.ignorePatterns)) {
170-
console.log(`[EXPLORER] Ignoring ${url} (matches ignore pattern)`);
171+
logger('[EXPLORER] Ignoring ' + url + ' (matches ignore pattern)');
171172
continue;
172173
}
173174

174175
// Skip external links if not following
175176
if (!fullConfig.followExternal) {
176177
const urlDomain = new URL(url).hostname;
177178
if (urlDomain !== startDomain) {
178-
console.log(`[EXPLORER] Skipping external link: ${url}`);
179+
logger('[EXPLORER] Skipping external link: ' + url);
179180
continue;
180181
}
181182
}
182183

183184
// Visit page
184-
console.log(
185-
`[EXPLORER] Visiting [${this.visited.size + 1}/${fullConfig.maxPages}] ${url} (depth ${depth})`,
186-
);
185+
logger('[EXPLORER] Visiting [' + (this.visited.size + 1) + '/' + fullConfig.maxPages + '] ' + url + ' (depth ' + depth + ')');
187186

188187
try {
189188
const pageInfo = await this.visitPage(page, url, depth, fullConfig);
@@ -195,23 +194,29 @@ export class AutonomousExplorer {
195194
this.visited.add(pageInfo.url);
196195
}
197196

198-
// Collect errors
199-
this.allErrors.push(...pageInfo.errors);
197+
// Collect errors (enforce size limit)
198+
if (this.allErrors.length < this.maxErrorsSize) {
199+
this.allErrors.push(...pageInfo.errors.slice(0, this.maxErrorsSize - this.allErrors.length));
200+
}
200201

201-
// Add links to queue for next depth level
202+
// Add links to queue for next depth level (enforce size limit)
202203
for (const link of pageInfo.links) {
204+
if (this.queue.length >= this.maxQueueSize) break;
203205
if (!this.visited.has(link) && !this.queue.some(item => item.url === link)) {
204206
this.queue.push({url: link, depth: depth + 1});
205207
}
206208
}
207209
} catch (error) {
208-
console.error(`[EXPLORER] Failed to visit ${url}:`, error);
209-
this.allErrors.push({
210-
message: `Failed to load: ${(error as Error).message}`,
211-
type: 'error',
212-
timestamp: Date.now(),
213-
url,
214-
});
210+
logger('[EXPLORER] Failed to visit ' + url + ': ' + String(error));
211+
// Enforce error array size limit
212+
if (this.allErrors.length < this.maxErrorsSize) {
213+
this.allErrors.push({
214+
message: `Failed to load: ${(error as Error).message}`,
215+
type: 'error',
216+
timestamp: Date.now(),
217+
url,
218+
});
219+
}
215220
}
216221
}
217222

@@ -226,9 +231,7 @@ export class AutonomousExplorer {
226231
allForms.push(...pageInfo.forms);
227232
}
228233

229-
console.log(
230-
`[EXPLORER] Exploration complete: ${this.visited.size} pages in ${explorationTime}ms`,
231-
);
234+
logger('[EXPLORER] Exploration complete: ' + this.visited.size + ' pages in ' + explorationTime + 'ms');
232235

233236
return {
234237
sitemap: this.sitemap,
@@ -292,7 +295,7 @@ export class AutonomousExplorer {
292295
page.on('console', consoleListener);
293296
page.on('pageerror', pageErrorListener);
294297
}
295-
let response = null;
298+
let response: any = null; // DevTools Response type - dynamic at runtime
296299
let status = 200; // Default to 200 for pre-loaded pages
297300

298301
// Normalize URLs for comparison (handle trailing slashes)
@@ -324,7 +327,7 @@ export class AutonomousExplorer {
324327
} else {
325328
// Skip navigation to preserve pre-loaded test content
326329
// But reload the HTML to retrigger scripts with our error listeners active
327-
console.log(`[EXPLORER] Skipping navigation (pre-loaded content detected)`);
330+
logger('[EXPLORER] Skipping navigation (pre-loaded content detected)');
328331

329332
if (config.detectErrors) {
330333
// Get current HTML and reload it to retrigger scripts with listeners active
@@ -455,7 +458,7 @@ export class AutonomousExplorer {
455458
const regex = new RegExp(pattern);
456459
return regex.test(url);
457460
} catch (error) {
458-
console.warn(`[EXPLORER] Invalid ignore pattern: ${pattern}`);
461+
logger('[EXPLORER] Invalid ignore pattern: ' + pattern);
459462
return false;
460463
}
461464
});

0 commit comments

Comments
 (0)