Skip to content

Commit 015894f

Browse files
committed
feat: add 10-minute idle timeout for MCP servers
Automatically stops MCP servers after 10 minutes of inactivity to conserve resources. Servers restart automatically on next message. This prevents resource exhaustion for users with many workspaces.
1 parent b784353 commit 015894f

File tree

3 files changed

+35
-2
lines changed

3 files changed

+35
-2
lines changed

docs/mcp-servers.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ This means you configure servers once per project, but each workspace (branch) g
6363

6464
- **Hot reload** — Config changes apply on your next message (no restart needed)
6565
- **Isolated** — Server processes run in the workspace directory with its environment
66-
- **Automatic lifecycle** — Servers start when first needed and stop when the workspace closes
66+
- **Lazy start** — Servers start when you send your first message in a workspace
67+
- **Idle timeout** — Servers stop after 10 minutes of inactivity to conserve resources, then restart automatically when needed
6768

6869
## Finding MCP Servers
6970

src/node/services/mcpServerManager.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { MCPConfigService } from "@/node/services/mcpConfigService";
88
import { createRuntime } from "@/node/runtime/runtimeFactory";
99

1010
const TEST_TIMEOUT_MS = 10_000;
11+
const IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
12+
const IDLE_CHECK_INTERVAL_MS = 60 * 1000; // Check every minute
1113

1214
/**
1315
* MCP CallToolResult content types (from @ai-sdk/mcp)
@@ -181,12 +183,38 @@ interface MCPServerInstance {
181183
interface WorkspaceServers {
182184
configSignature: string;
183185
instances: Map<string, MCPServerInstance>;
186+
lastActivity: number;
184187
}
185188

186189
export class MCPServerManager {
187190
private readonly workspaceServers = new Map<string, WorkspaceServers>();
191+
private readonly idleCheckInterval: ReturnType<typeof setInterval>;
188192

189-
constructor(private readonly configService: MCPConfigService) {}
193+
constructor(private readonly configService: MCPConfigService) {
194+
this.idleCheckInterval = setInterval(() => this.cleanupIdleServers(), IDLE_CHECK_INTERVAL_MS);
195+
}
196+
197+
/**
198+
* Stop the idle cleanup interval. Call when shutting down.
199+
*/
200+
dispose(): void {
201+
clearInterval(this.idleCheckInterval);
202+
}
203+
204+
private cleanupIdleServers(): void {
205+
const now = Date.now();
206+
for (const [workspaceId, entry] of this.workspaceServers) {
207+
if (entry.instances.size === 0) continue;
208+
const idleMs = now - entry.lastActivity;
209+
if (idleMs >= IDLE_TIMEOUT_MS) {
210+
log.info("[MCP] Stopping idle servers", {
211+
workspaceId,
212+
idleMinutes: Math.round(idleMs / 60_000),
213+
});
214+
void this.stopServers(workspaceId);
215+
}
216+
}
217+
}
190218

191219
/**
192220
* List configured MCP servers for a project (name -> command).
@@ -209,6 +237,8 @@ export class MCPServerManager {
209237

210238
const existing = this.workspaceServers.get(workspaceId);
211239
if (existing?.configSignature === signature) {
240+
// Update activity timestamp to prevent idle cleanup
241+
existing.lastActivity = Date.now();
212242
log.debug("[MCP] Using cached servers", { workspaceId, serverCount: serverNames.length });
213243
return this.collectTools(existing.instances);
214244
}
@@ -222,6 +252,7 @@ export class MCPServerManager {
222252
this.workspaceServers.set(workspaceId, {
223253
configSignature: signature,
224254
instances,
255+
lastActivity: Date.now(),
225256
});
226257
return this.collectTools(instances);
227258
}

src/node/services/serviceContainer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export class ServiceContainer {
122122
* Terminates all background processes to prevent orphans.
123123
*/
124124
async dispose(): Promise<void> {
125+
this.mcpServerManager.dispose();
125126
await this.backgroundProcessManager.terminateAll();
126127
}
127128
}

0 commit comments

Comments
 (0)