Skip to content

Commit d97c1da

Browse files
ktnytclaude
andcommitted
✨ feat: add LSP server auto-restart functionality
- Add restartInterval option to LSPServerConfig for automatic server restarts - Implement timer-based restart mechanism to prevent long-running server degradation - Add comprehensive test coverage for restart functionality - Update documentation with configuration examples and guidelines - Particularly beneficial for Python Language Server (pylsp) stability - Version bump to 0.4.2 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 4c02180 commit d97c1da

File tree

10 files changed

+347
-9
lines changed

10 files changed

+347
-9
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.4.2] - 2025-06-29
9+
10+
### Added
11+
12+
- **LSP Server Auto-Restart**: Added `restartInterval` option to server configuration for automatic LSP server restarts to prevent long-running server degradation
13+
- Configurable restart intervals in minutes with minimum 0.1 minute (6 seconds) for testing
14+
- Comprehensive test coverage for restart functionality including timer setup, configuration validation, and cleanup
15+
16+
### Enhanced
17+
18+
- Improved LSP server stability for long-running sessions, particularly beneficial for Python Language Server (pylsp)
19+
- Updated documentation with configuration examples and restart interval guidelines
20+
- **Setup Wizard Improvements**: Enhanced file extension detection with comprehensive .gitignore support
21+
- Improved project structure scanning to exclude common build artifacts, dependencies, and temporary files
22+
- Better accuracy in detecting project's primary programming languages for LSP server configuration
23+
824
## [0.4.1] - 2025-06-28
925

1026
### Added

CLAUDE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ Each server config requires:
109109
- `extensions`: File extensions to handle (array)
110110
- `command`: Command array to spawn LSP server
111111
- `rootDir`: Working directory for LSP server (optional)
112+
- `restartInterval`: Auto-restart interval in minutes (optional, helps with long-running server stability, minimum 1 minute)
113+
114+
### Example Configuration
115+
116+
```json
117+
{
118+
"servers": [
119+
{
120+
"extensions": ["py"],
121+
"command": ["pylsp"],
122+
"restartInterval": 5
123+
},
124+
{
125+
"extensions": ["ts", "tsx", "js", "jsx"],
126+
"command": ["typescript-language-server", "--stdio"],
127+
"restartInterval": 10
128+
}
129+
]
130+
}
131+
```
112132

113133
## Code Quality & Testing
114134

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,40 @@ Result: 12 files will be updated with the new name
478478

479479
## 🔍 Troubleshooting
480480

481+
### Known Issues
482+
483+
<details>
484+
<summary>🐍 Python LSP Server (pylsp) Performance Degradation</summary>
485+
486+
**Problem**: The Python Language Server (pylsp) may become slow or unresponsive after extended use (several hours), affecting symbol resolution and code navigation.
487+
488+
**Symptoms**:
489+
- Slow or missing "go to definition" results for Python files
490+
- Delayed or incomplete symbol references
491+
- General responsiveness issues with Python code analysis
492+
493+
**Solution**: Use the auto-restart feature to periodically restart the pylsp server:
494+
495+
Add `restartInterval` to your Python server configuration:
496+
497+
```json
498+
{
499+
"servers": [
500+
{
501+
"extensions": ["py", "pyi"],
502+
"command": ["pylsp"],
503+
"restartInterval": 5
504+
}
505+
]
506+
}
507+
```
508+
509+
This will automatically restart the Python LSP server every 5 minutes, maintaining optimal performance for long coding sessions.
510+
511+
**Note**: The setup wizard automatically configures this for Python servers when detected.
512+
513+
</details>
514+
481515
### Common Issues
482516

483517
<details>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cclsp",
3-
"version": "0.4.1",
3+
"version": "0.4.2",
44
"description": "MCP server for accessing LSP functionality",
55
"main": "dist/index.js",
66
"bin": {

src/language-servers.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface LanguageServerConfig {
77
rootDir?: string;
88
description?: string;
99
installRequired?: boolean;
10+
restartInterval?: number; // Default restart interval in minutes
1011
}
1112

1213
export const LANGUAGE_SERVERS: LanguageServerConfig[] = [
@@ -27,6 +28,7 @@ export const LANGUAGE_SERVERS: LanguageServerConfig[] = [
2728
installInstructions: 'pip install python-lsp-server',
2829
description: 'Python Language Server Protocol implementation',
2930
installRequired: false,
31+
restartInterval: 5, // Auto-restart every 5 minutes to prevent performance degradation
3032
},
3133
{
3234
name: 'go',
@@ -153,10 +155,24 @@ export function generateConfig(selectedLanguages: string[]): object {
153155
);
154156

155157
return {
156-
servers: selectedServers.map((server) => ({
157-
extensions: server.extensions,
158-
command: server.command,
159-
rootDir: server.rootDir || '.',
160-
})),
158+
servers: selectedServers.map((server) => {
159+
const config: {
160+
extensions: string[];
161+
command: string[];
162+
rootDir: string;
163+
restartInterval?: number;
164+
} = {
165+
extensions: server.extensions,
166+
command: server.command,
167+
rootDir: server.rootDir || '.',
168+
};
169+
170+
// Add restartInterval if specified for the server
171+
if (server.restartInterval) {
172+
config.restartInterval = server.restartInterval;
173+
}
174+
175+
return config;
176+
}),
161177
};
162178
}

src/lsp-client.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,4 +435,94 @@ describe('LSPClient', () => {
435435
getDocumentSymbolsSpy.mockRestore();
436436
});
437437
});
438+
439+
describe('Server restart functionality', () => {
440+
it('should setup restart timer when restartInterval is configured', () => {
441+
const client = new LSPClient(TEST_CONFIG_PATH);
442+
443+
// Mock setTimeout to verify timer is set
444+
const setTimeoutSpy = spyOn(global, 'setTimeout').mockImplementation((() => 123) as any);
445+
446+
const mockServerState = {
447+
process: { kill: jest.fn() },
448+
initialized: true,
449+
initializationPromise: Promise.resolve(),
450+
openFiles: new Set(),
451+
startTime: Date.now(),
452+
config: {
453+
extensions: ['ts'],
454+
command: ['echo', 'mock'],
455+
restartInterval: 0.1, // 0.1 minutes
456+
},
457+
restartTimer: undefined,
458+
};
459+
460+
try {
461+
// Call setupRestartTimer directly
462+
(client as any).setupRestartTimer(mockServerState);
463+
464+
// Verify setTimeout was called with correct interval (0.1 minutes = 6000ms)
465+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 6000);
466+
} finally {
467+
setTimeoutSpy.mockRestore();
468+
client.dispose();
469+
}
470+
});
471+
472+
it('should not setup restart timer when restartInterval is not configured', () => {
473+
const client = new LSPClient(TEST_CONFIG_PATH);
474+
475+
// Mock setTimeout to verify timer is NOT set
476+
const setTimeoutSpy = spyOn(global, 'setTimeout').mockImplementation((() => 123) as any);
477+
478+
const mockServerState = {
479+
process: { kill: jest.fn() },
480+
initialized: true,
481+
initializationPromise: Promise.resolve(),
482+
openFiles: new Set(),
483+
startTime: Date.now(),
484+
config: {
485+
extensions: ['ts'],
486+
command: ['echo', 'mock'],
487+
// No restartInterval
488+
},
489+
restartTimer: undefined,
490+
};
491+
492+
try {
493+
// Call setupRestartTimer directly
494+
(client as any).setupRestartTimer(mockServerState);
495+
496+
// Verify setTimeout was NOT called
497+
expect(setTimeoutSpy).not.toHaveBeenCalled();
498+
} finally {
499+
setTimeoutSpy.mockRestore();
500+
client.dispose();
501+
}
502+
});
503+
504+
it('should clear restart timer when disposing client', async () => {
505+
const client = new LSPClient(TEST_CONFIG_PATH);
506+
507+
const mockTimer = setTimeout(() => {}, 1000);
508+
const mockServerState = {
509+
process: { kill: jest.fn() },
510+
restartTimer: mockTimer,
511+
};
512+
513+
// Mock servers map to include our test server state
514+
const serversMap = new Map();
515+
serversMap.set('test-key', mockServerState);
516+
(client as any).servers = serversMap;
517+
518+
const clearTimeoutSpy = spyOn(global, 'clearTimeout');
519+
520+
client.dispose();
521+
522+
expect(clearTimeoutSpy).toHaveBeenCalledWith(mockTimer);
523+
expect(mockServerState.process.kill).toHaveBeenCalled();
524+
525+
clearTimeoutSpy.mockRestore();
526+
});
527+
});
438528
});

src/lsp-client.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ interface ServerState {
3030
initialized: boolean;
3131
initializationPromise: Promise<void>;
3232
openFiles: Set<string>;
33+
startTime: number;
34+
config: LSPServerConfig;
35+
restartTimer?: NodeJS.Timeout;
3336
}
3437

3538
export class LSPClient {
@@ -128,6 +131,9 @@ export class LSPClient {
128131
initialized: false,
129132
initializationPromise,
130133
openFiles: new Set(),
134+
startTime: Date.now(),
135+
config: serverConfig,
136+
restartTimer: undefined,
131137
};
132138

133139
let buffer = '';
@@ -237,6 +243,10 @@ export class LSPClient {
237243

238244
serverState.initialized = true;
239245
initializationResolve?.();
246+
247+
// Set up auto-restart timer if configured
248+
this.setupRestartTimer(serverState);
249+
240250
return serverState;
241251
}
242252

@@ -290,6 +300,54 @@ export class LSPClient {
290300
this.sendMessage(process, message);
291301
}
292302

303+
private setupRestartTimer(serverState: ServerState): void {
304+
if (serverState.config.restartInterval && serverState.config.restartInterval > 0) {
305+
// Minimum interval is 0.1 minutes (6 seconds) for testing, practical minimum is 1 minute
306+
const minInterval = 0.1;
307+
const actualInterval = Math.max(serverState.config.restartInterval, minInterval);
308+
const intervalMs = actualInterval * 60 * 1000; // Convert minutes to milliseconds
309+
310+
process.stderr.write(
311+
`[DEBUG setupRestartTimer] Setting up restart timer for ${actualInterval} minutes\n`
312+
);
313+
314+
serverState.restartTimer = setTimeout(() => {
315+
this.restartServer(serverState);
316+
}, intervalMs);
317+
}
318+
}
319+
320+
private async restartServer(serverState: ServerState): Promise<void> {
321+
const key = JSON.stringify(serverState.config);
322+
process.stderr.write(
323+
`[DEBUG restartServer] Restarting LSP server for ${serverState.config.command.join(' ')}\n`
324+
);
325+
326+
// Clear existing timer
327+
if (serverState.restartTimer) {
328+
clearTimeout(serverState.restartTimer);
329+
serverState.restartTimer = undefined;
330+
}
331+
332+
// Terminate old server
333+
serverState.process.kill();
334+
335+
// Remove from servers map
336+
this.servers.delete(key);
337+
338+
try {
339+
// Start new server
340+
const newServerState = await this.startServer(serverState.config);
341+
this.servers.set(key, newServerState);
342+
343+
process.stderr.write(
344+
`[DEBUG restartServer] Successfully restarted LSP server for ${serverState.config.command.join(' ')}\n`
345+
);
346+
} catch (error) {
347+
process.stderr.write(`[DEBUG restartServer] Failed to restart LSP server: ${error}\n`);
348+
}
349+
}
350+
293351
private async ensureFileOpen(serverState: ServerState, filePath: string): Promise<void> {
294352
if (serverState.openFiles.has(filePath)) {
295353
process.stderr.write(`[DEBUG ensureFileOpen] File already open: ${filePath}\n`);
@@ -983,6 +1041,10 @@ export class LSPClient {
9831041

9841042
dispose(): void {
9851043
for (const serverState of this.servers.values()) {
1044+
// Clear restart timer if exists
1045+
if (serverState.restartTimer) {
1046+
clearTimeout(serverState.restartTimer);
1047+
}
9861048
serverState.process.kill();
9871049
}
9881050
this.servers.clear();

src/setup.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface GeneratedConfig {
88
extensions: string[];
99
command: string[];
1010
rootDir: string;
11+
restartInterval?: number;
1112
}>;
1213
}
1314

@@ -120,6 +121,28 @@ describe('generateConfig', () => {
120121
expect(serverNames).toContain('go');
121122
});
122123

124+
test('should include restartInterval for Python server', () => {
125+
const config = generateConfig(['python']);
126+
expect(config).toHaveProperty('servers');
127+
expect(Array.isArray((config as GeneratedConfig).servers)).toBe(true);
128+
expect((config as GeneratedConfig).servers).toHaveLength(1);
129+
130+
const pythonServer = (config as GeneratedConfig).servers[0];
131+
expect(pythonServer?.extensions).toContain('py');
132+
expect(pythonServer?.restartInterval).toBe(5);
133+
});
134+
135+
test('should not include restartInterval for servers without it configured', () => {
136+
const config = generateConfig(['typescript']);
137+
expect(config).toHaveProperty('servers');
138+
expect(Array.isArray((config as GeneratedConfig).servers)).toBe(true);
139+
expect((config as GeneratedConfig).servers).toHaveLength(1);
140+
141+
const typescriptServer = (config as GeneratedConfig).servers[0];
142+
expect(typescriptServer?.extensions).toContain('ts');
143+
expect(typescriptServer?.restartInterval).toBeUndefined();
144+
});
145+
123146
test('should handle invalid language names gracefully', () => {
124147
const config = generateConfig(['nonexistent', 'typescript']);
125148
expect(config).toHaveProperty('servers');

0 commit comments

Comments
 (0)