@theupsider/lsp-mcp 0.1.0 → 0.1.3
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.
- package/dist/__tests__/index.test.js +2 -1
- package/dist/detection/language-detector.js +7 -8
- package/dist/detection/language-registry.js +38 -0
- package/dist/lsp/__tests__/lifecycle-manager.test.js +10 -8
- package/dist/lsp/diagnostic-store.js +49 -0
- package/dist/lsp/lifecycle-manager.js +42 -194
- package/dist/lsp/lsp-client.js +131 -44
- package/dist/lsp/server-supervisor.js +152 -0
- package/dist/mcp/__tests__/formatters.test.js +1 -1
- package/dist/mcp/__tests__/read-tools.test.js +11 -9
- package/dist/mcp/__tests__/server.test.js +4 -1
- package/dist/mcp/__tests__/write-tools.test.js +10 -6
- package/dist/mcp/formatters.js +1 -1
- package/dist/mcp/server.js +4 -3
- package/dist/mcp/tools/read-tools.js +14 -3
- package/dist/mcp/tools/shared.js +0 -44
- package/dist/mcp/tools/write-tools.js +5 -5
- package/package.json +1 -1
|
@@ -26,8 +26,9 @@ describe('index entrypoint', () => {
|
|
|
26
26
|
it('prints version and exits for --version', async () => {
|
|
27
27
|
const stdout = jest.fn();
|
|
28
28
|
const exit = jest.fn();
|
|
29
|
+
const { version } = jest.requireActual('../../package.json');
|
|
29
30
|
await (0, index_1.main)(['--version'], {}, { stdout, exit });
|
|
30
|
-
expect(stdout).toHaveBeenCalledWith(
|
|
31
|
+
expect(stdout).toHaveBeenCalledWith(`${version}\n`);
|
|
31
32
|
expect(exit).toHaveBeenCalledWith(0);
|
|
32
33
|
});
|
|
33
34
|
it('boots lifecycle, logs startup report, and starts the MCP server', async () => {
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.detectLanguages = detectLanguages;
|
|
7
7
|
const promises_1 = require("node:fs/promises");
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const language_registry_1 = require("./language-registry");
|
|
9
10
|
const MARKER_DEFINITIONS = [
|
|
10
11
|
{ language: 'javascript', markers: ['package.json'] },
|
|
11
12
|
{ language: 'typescript', markers: ['tsconfig.json'] },
|
|
@@ -19,10 +20,7 @@ const MARKER_DEFINITIONS = [
|
|
|
19
20
|
{ language: 'kotlin', markers: ['build.gradle.kts'] },
|
|
20
21
|
{ language: 'swift', markers: ['Package.swift'] }
|
|
21
22
|
];
|
|
22
|
-
const
|
|
23
|
-
{ language: 'c', extensions: ['.c', '.h'] },
|
|
24
|
-
{ language: 'cpp', extensions: ['.cpp', '.hpp', '.cc'] }
|
|
25
|
-
];
|
|
23
|
+
const EXTENSION_LANGUAGES = ['c', 'cpp'];
|
|
26
24
|
async function detectLanguages(projectRoot) {
|
|
27
25
|
const entries = await collectEntries(projectRoot, projectRoot);
|
|
28
26
|
const markerMatches = new Map();
|
|
@@ -64,14 +62,15 @@ async function detectLanguages(projectRoot) {
|
|
|
64
62
|
});
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
|
-
for (const
|
|
65
|
+
for (const language of EXTENSION_LANGUAGES) {
|
|
66
|
+
const extensions = (0, language_registry_1.extensionsForLanguage)(language);
|
|
68
67
|
const matches = entries
|
|
69
|
-
.filter((entry) =>
|
|
68
|
+
.filter((entry) => extensions.some((extension) => entry.relativePath.endsWith(extension)))
|
|
70
69
|
.map((entry) => entry.relativePath)
|
|
71
|
-
.sort((left, right) => compareByExtensionPriority(left, right,
|
|
70
|
+
.sort((left, right) => compareByExtensionPriority(left, right, extensions));
|
|
72
71
|
if (matches.length > 0) {
|
|
73
72
|
detected.push({
|
|
74
|
-
language
|
|
73
|
+
language,
|
|
75
74
|
confidence: 'extension',
|
|
76
75
|
markers: matches
|
|
77
76
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KNOWN_EXTENSIONS = void 0;
|
|
4
|
+
exports.extensionToLanguage = extensionToLanguage;
|
|
5
|
+
exports.extensionToLanguageId = extensionToLanguageId;
|
|
6
|
+
exports.extensionsForLanguage = extensionsForLanguage;
|
|
7
|
+
const EXTENSION_MAP = {
|
|
8
|
+
'.ts': { language: 'typescript', languageId: 'typescript' },
|
|
9
|
+
'.tsx': { language: 'typescript', languageId: 'typescriptreact' },
|
|
10
|
+
'.js': { language: 'javascript', languageId: 'javascript' },
|
|
11
|
+
'.jsx': { language: 'javascript', languageId: 'javascriptreact' },
|
|
12
|
+
'.py': { language: 'python', languageId: 'python' },
|
|
13
|
+
'.cs': { language: 'csharp', languageId: 'csharp' },
|
|
14
|
+
'.java': { language: 'java', languageId: 'java' },
|
|
15
|
+
'.go': { language: 'go', languageId: 'go' },
|
|
16
|
+
'.rs': { language: 'rust', languageId: 'rust' },
|
|
17
|
+
'.c': { language: 'c', languageId: 'c' },
|
|
18
|
+
'.h': { language: 'c', languageId: 'c' },
|
|
19
|
+
'.cpp': { language: 'cpp', languageId: 'cpp' },
|
|
20
|
+
'.hpp': { language: 'cpp', languageId: 'cpp' },
|
|
21
|
+
'.cc': { language: 'cpp', languageId: 'cpp' },
|
|
22
|
+
'.rb': { language: 'ruby', languageId: 'ruby' },
|
|
23
|
+
'.php': { language: 'php', languageId: 'php' },
|
|
24
|
+
'.kt': { language: 'kotlin', languageId: 'kotlin' },
|
|
25
|
+
'.swift': { language: 'swift', languageId: 'swift' }
|
|
26
|
+
};
|
|
27
|
+
function extensionToLanguage(ext) {
|
|
28
|
+
return EXTENSION_MAP[ext.toLowerCase()]?.language;
|
|
29
|
+
}
|
|
30
|
+
function extensionToLanguageId(ext) {
|
|
31
|
+
return EXTENSION_MAP[ext.toLowerCase()]?.languageId ?? 'plaintext';
|
|
32
|
+
}
|
|
33
|
+
function extensionsForLanguage(language) {
|
|
34
|
+
return Object.entries(EXTENSION_MAP)
|
|
35
|
+
.filter(([, entry]) => entry.language === language)
|
|
36
|
+
.map(([ext]) => ext);
|
|
37
|
+
}
|
|
38
|
+
exports.KNOWN_EXTENSIONS = new Set(Object.keys(EXTENSION_MAP));
|
|
@@ -21,6 +21,8 @@ class MockLspClient extends node_events_1.EventEmitter {
|
|
|
21
21
|
request = jest.fn().mockResolvedValue({ items: [] });
|
|
22
22
|
isReady = jest.fn().mockReturnValue(true);
|
|
23
23
|
getCapabilities = jest.fn().mockReturnValue({ hoverProvider: true });
|
|
24
|
+
ensureDidOpen = jest.fn().mockResolvedValue(undefined);
|
|
25
|
+
waitForDiagnosticsPublish = jest.fn().mockResolvedValue(undefined);
|
|
24
26
|
}
|
|
25
27
|
describe('LifecycleManager', () => {
|
|
26
28
|
beforeEach(() => {
|
|
@@ -116,7 +118,7 @@ describe('LifecycleManager', () => {
|
|
|
116
118
|
const getLspCandidates = jest.requireMock('../../detection/lsp-mapping').getLspCandidates;
|
|
117
119
|
const LspClient = jest.requireMock('../lsp-client').LspClient;
|
|
118
120
|
const firstClient = new MockLspClient();
|
|
119
|
-
firstClient.
|
|
121
|
+
firstClient.isReady.mockReturnValue(false);
|
|
120
122
|
const secondClient = new MockLspClient();
|
|
121
123
|
detectLanguages.mockResolvedValue([{ language: 'typescript', confidence: 'marker', markers: ['tsconfig.json'] }]);
|
|
122
124
|
findAvailableLsp.mockResolvedValue({ cmd: 'typescript-language-server', args: ['--stdio'], pkg: 'typescript-language-server', mgr: 'npm' });
|
|
@@ -126,7 +128,7 @@ describe('LifecycleManager', () => {
|
|
|
126
128
|
const manager = new lifecycle_manager_1.LifecycleManager('/workspace/project', 'debug');
|
|
127
129
|
await manager.start();
|
|
128
130
|
await jest.advanceTimersByTimeAsync(30000);
|
|
129
|
-
expect(firstClient.
|
|
131
|
+
expect(firstClient.isReady).toHaveBeenCalled();
|
|
130
132
|
expect(secondClient.start).toHaveBeenCalled();
|
|
131
133
|
expect(manager.getClient('typescript')).toBe(secondClient);
|
|
132
134
|
await manager.shutdown();
|
|
@@ -222,13 +224,13 @@ describe('LifecycleManager', () => {
|
|
|
222
224
|
const manager = new lifecycle_manager_1.LifecycleManager('/workspace/project', 'debug');
|
|
223
225
|
await manager.start();
|
|
224
226
|
const managerAccess = manager;
|
|
225
|
-
const
|
|
226
|
-
expect(
|
|
227
|
-
if (!
|
|
228
|
-
throw new Error('Expected typescript
|
|
227
|
+
const supervisor = managerAccess.supervisors.get('typescript');
|
|
228
|
+
expect(supervisor).toBeDefined();
|
|
229
|
+
if (!supervisor) {
|
|
230
|
+
throw new Error('Expected typescript supervisor');
|
|
229
231
|
}
|
|
230
|
-
|
|
231
|
-
await
|
|
232
|
+
supervisor.restartCount = 3;
|
|
233
|
+
await supervisor.restart('too many restarts');
|
|
232
234
|
expect(manager.getClient('typescript')).toBeNull();
|
|
233
235
|
expect(manager.getHealth()).toEqual([
|
|
234
236
|
{
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DiagnosticStore = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const language_registry_1 = require("../detection/language-registry");
|
|
9
|
+
const uri_1 = require("../utils/uri");
|
|
10
|
+
class DiagnosticStore {
|
|
11
|
+
diagnostics = new Map();
|
|
12
|
+
store(params) {
|
|
13
|
+
if (!isPublishDiagnosticsParams(params)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.diagnostics.set(params.uri, cloneDiagnostics(params.diagnostics));
|
|
17
|
+
}
|
|
18
|
+
getForFile(filePath) {
|
|
19
|
+
const uri = (0, uri_1.pathToUri)(filePath);
|
|
20
|
+
return (this.diagnostics.get(uri) ?? []).map((diagnostic) => ({ ...diagnostic, uri }));
|
|
21
|
+
}
|
|
22
|
+
getForWorkspace(language) {
|
|
23
|
+
const allowedExtensions = language ? (0, language_registry_1.extensionsForLanguage)(language) : null;
|
|
24
|
+
return Array.from(this.diagnostics.entries())
|
|
25
|
+
.filter(([uri]) => {
|
|
26
|
+
if (!allowedExtensions) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return allowedExtensions.includes(node_path_1.default.extname((0, uri_1.uriToPath)(uri)).toLowerCase());
|
|
30
|
+
})
|
|
31
|
+
.flatMap(([uri, diagnostics]) => diagnostics.map((diagnostic) => ({ ...diagnostic, uri })))
|
|
32
|
+
.sort((left, right) => (left.severity ?? 4) - (right.severity ?? 4));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.DiagnosticStore = DiagnosticStore;
|
|
36
|
+
function isPublishDiagnosticsParams(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null
|
|
38
|
+
&& 'uri' in value && typeof value.uri === 'string'
|
|
39
|
+
&& 'diagnostics' in value && Array.isArray(value.diagnostics);
|
|
40
|
+
}
|
|
41
|
+
function cloneDiagnostics(diagnostics) {
|
|
42
|
+
return diagnostics.map((diagnostic) => ({
|
|
43
|
+
...diagnostic,
|
|
44
|
+
range: {
|
|
45
|
+
start: { ...diagnostic.range.start },
|
|
46
|
+
end: { ...diagnostic.range.end }
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
@@ -6,35 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.LifecycleManager = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const language_detector_1 = require("../detection/language-detector");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const lsp_client_1 = require("./lsp-client");
|
|
13
|
-
const EXTENSION_LANGUAGE_MAP = {
|
|
14
|
-
'.ts': 'typescript',
|
|
15
|
-
'.tsx': 'typescript',
|
|
16
|
-
'.js': 'javascript',
|
|
17
|
-
'.jsx': 'javascript',
|
|
18
|
-
'.py': 'python',
|
|
19
|
-
'.cs': 'csharp',
|
|
20
|
-
'.java': 'java',
|
|
21
|
-
'.go': 'go',
|
|
22
|
-
'.rs': 'rust',
|
|
23
|
-
'.c': 'c',
|
|
24
|
-
'.h': 'c',
|
|
25
|
-
'.cpp': 'cpp',
|
|
26
|
-
'.hpp': 'cpp',
|
|
27
|
-
'.cc': 'cpp',
|
|
28
|
-
'.rb': 'ruby',
|
|
29
|
-
'.php': 'php',
|
|
30
|
-
'.kt': 'kotlin',
|
|
31
|
-
'.swift': 'swift'
|
|
32
|
-
};
|
|
9
|
+
const language_registry_1 = require("../detection/language-registry");
|
|
10
|
+
const diagnostic_store_1 = require("./diagnostic-store");
|
|
11
|
+
const server_supervisor_1 = require("./server-supervisor");
|
|
33
12
|
class LifecycleManager {
|
|
34
13
|
projectRoot;
|
|
35
14
|
logLevel;
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
supervisors = new Map();
|
|
16
|
+
store = new diagnostic_store_1.DiagnosticStore();
|
|
38
17
|
constructor(projectRoot, logLevel) {
|
|
39
18
|
this.projectRoot = projectRoot;
|
|
40
19
|
this.logLevel = normalizeLogLevel(logLevel);
|
|
@@ -44,197 +23,80 @@ class LifecycleManager {
|
|
|
44
23
|
await promiseWithTimeout(startPromise, 30000, 'Lifecycle start timed out');
|
|
45
24
|
}
|
|
46
25
|
async ensureLanguage(language) {
|
|
47
|
-
if (this.
|
|
26
|
+
if (this.supervisors.has(language)) {
|
|
48
27
|
return;
|
|
49
28
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
status: 'starting',
|
|
54
|
-
serverDef: null,
|
|
55
|
-
restartCount: 0,
|
|
56
|
-
healthInterval: null
|
|
57
|
-
};
|
|
58
|
-
this.states.set(language, state);
|
|
59
|
-
await this.startLanguage(state);
|
|
29
|
+
const supervisor = this.createSupervisor(language);
|
|
30
|
+
this.supervisors.set(language, supervisor);
|
|
31
|
+
await supervisor.start();
|
|
60
32
|
}
|
|
61
33
|
async ensureLanguageForFile(filePath) {
|
|
62
|
-
const language =
|
|
34
|
+
const language = (0, language_registry_1.extensionToLanguage)(node_path_1.default.extname(filePath));
|
|
63
35
|
if (language) {
|
|
64
36
|
await this.ensureLanguage(language);
|
|
65
37
|
}
|
|
66
38
|
}
|
|
67
39
|
getClient(language) {
|
|
68
|
-
return this.
|
|
40
|
+
return this.supervisors.get(language)?.getClient() ?? null;
|
|
69
41
|
}
|
|
70
42
|
getClientForFile(filePath) {
|
|
71
|
-
const language =
|
|
43
|
+
const language = (0, language_registry_1.extensionToLanguage)(node_path_1.default.extname(filePath));
|
|
72
44
|
if (language) {
|
|
73
45
|
return this.getClient(language);
|
|
74
46
|
}
|
|
75
47
|
return null;
|
|
76
48
|
}
|
|
77
49
|
getHealth() {
|
|
78
|
-
return Array.from(this.
|
|
79
|
-
language: state.language,
|
|
80
|
-
status: state.status,
|
|
81
|
-
error: state.error,
|
|
82
|
-
capabilities: state.capabilities
|
|
83
|
-
}));
|
|
50
|
+
return Array.from(this.supervisors.values()).map((supervisor) => supervisor.getHealth());
|
|
84
51
|
}
|
|
85
52
|
getReadyClients(language) {
|
|
86
|
-
return Array.from(this.
|
|
87
|
-
.filter((
|
|
88
|
-
|
|
53
|
+
return Array.from(this.supervisors.values())
|
|
54
|
+
.filter((supervisor) => {
|
|
55
|
+
const health = supervisor.getHealth();
|
|
56
|
+
return health.status === 'ready' && (!language || supervisor.language === language);
|
|
57
|
+
})
|
|
58
|
+
.flatMap((supervisor) => {
|
|
59
|
+
const client = supervisor.getClient();
|
|
60
|
+
return client ? [client] : [];
|
|
61
|
+
});
|
|
89
62
|
}
|
|
90
63
|
getFileDiagnostics(filePath) {
|
|
91
|
-
|
|
92
|
-
return (this.diagnostics.get(uri) ?? []).map((diagnostic) => ({ ...diagnostic, uri }));
|
|
64
|
+
return this.store.getForFile(filePath);
|
|
93
65
|
}
|
|
94
66
|
getWorkspaceDiagnostics(language) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return allowedExtensions.includes(node_path_1.default.extname((0, uri_1.uriToPath)(uri)).toLowerCase());
|
|
106
|
-
})
|
|
107
|
-
.flatMap(([uri, diagnostics]) => diagnostics.map((diagnostic) => ({ ...diagnostic, uri })))
|
|
108
|
-
.sort((left, right) => (left.severity ?? 4) - (right.severity ?? 4));
|
|
67
|
+
return this.store.getForWorkspace(language);
|
|
68
|
+
}
|
|
69
|
+
async ensureSeedFilesOpen() {
|
|
70
|
+
await Promise.all(Array.from(this.supervisors.entries()).map(async ([language, supervisor]) => {
|
|
71
|
+
const client = supervisor.getClient();
|
|
72
|
+
if (!client)
|
|
73
|
+
return;
|
|
74
|
+
const extensions = (0, language_registry_1.extensionsForLanguage)(language);
|
|
75
|
+
await client.ensureSeedFileOpen(extensions);
|
|
76
|
+
}));
|
|
109
77
|
}
|
|
110
78
|
async shutdown() {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
clearInterval(state.healthInterval);
|
|
114
|
-
state.healthInterval = null;
|
|
115
|
-
}
|
|
116
|
-
return state.client ? [state.client] : [];
|
|
117
|
-
});
|
|
118
|
-
const results = await Promise.allSettled(clients.map(async (client) => await promiseWithTimeout(client.shutdown(), 5000, 'LSP shutdown timed out')));
|
|
79
|
+
const supervisors = Array.from(this.supervisors.values());
|
|
80
|
+
const results = await Promise.allSettled(supervisors.map(async (supervisor) => await promiseWithTimeout(supervisor.shutdown(), 5000, 'LSP shutdown timed out')));
|
|
119
81
|
const errors = results.filter((result) => result.status === 'rejected').length;
|
|
120
|
-
process.stderr.write(`{"timestamp":"${new Date().toISOString()}","level":"info","event":"Shutdown: ${
|
|
82
|
+
process.stderr.write(`{"timestamp":"${new Date().toISOString()}","level":"info","event":"Shutdown: ${supervisors.length - errors} LSP-Server beendet, ${errors} Fehler"}\n`);
|
|
121
83
|
}
|
|
122
84
|
async startInternal(languages) {
|
|
123
85
|
const detected = languages
|
|
124
86
|
? languages.map((language) => ({ language }))
|
|
125
87
|
: await (0, language_detector_1.detectLanguages)(this.projectRoot);
|
|
126
88
|
for (const entry of detected) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
status: 'starting',
|
|
131
|
-
serverDef: null,
|
|
132
|
-
restartCount: 0,
|
|
133
|
-
healthInterval: null
|
|
134
|
-
};
|
|
135
|
-
this.states.set(entry.language, state);
|
|
136
|
-
await this.startLanguage(state);
|
|
89
|
+
const supervisor = this.createSupervisor(entry.language);
|
|
90
|
+
this.supervisors.set(entry.language, supervisor);
|
|
91
|
+
await supervisor.start();
|
|
137
92
|
}
|
|
138
93
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!serverDef) {
|
|
142
|
-
state.status = 'error';
|
|
143
|
-
state.error = `No LSP server available for ${state.language}`;
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
state.serverDef = serverDef;
|
|
147
|
-
const client = new lsp_client_1.LspClient(serverDef, this.projectRoot, this.logLevel);
|
|
148
|
-
state.client = client;
|
|
149
|
-
client.on('crash', async () => {
|
|
150
|
-
await this.restartLanguage(state, `LSP server crashed for ${state.language}`);
|
|
151
|
-
});
|
|
152
|
-
client.on('error', (error) => {
|
|
153
|
-
state.status = 'error';
|
|
154
|
-
state.error = error instanceof Error ? error.message : 'Unknown LSP error';
|
|
155
|
-
});
|
|
156
|
-
client.on('notification', (method, params) => {
|
|
94
|
+
createSupervisor(language) {
|
|
95
|
+
return new server_supervisor_1.ServerSupervisor(language, this.projectRoot, this.logLevel, (method, params) => {
|
|
157
96
|
if (method === 'textDocument/publishDiagnostics') {
|
|
158
|
-
this.
|
|
97
|
+
this.store.store(params);
|
|
159
98
|
}
|
|
160
99
|
});
|
|
161
|
-
try {
|
|
162
|
-
await client.start();
|
|
163
|
-
state.status = 'ready';
|
|
164
|
-
state.error = undefined;
|
|
165
|
-
state.capabilities = client.getCapabilities() ?? undefined;
|
|
166
|
-
this.startHealthChecks(state);
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
state.status = 'error';
|
|
170
|
-
state.error = error instanceof Error ? error.message : 'Unknown LSP startup error';
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
startHealthChecks(state) {
|
|
174
|
-
if (state.healthInterval) {
|
|
175
|
-
clearInterval(state.healthInterval);
|
|
176
|
-
}
|
|
177
|
-
state.healthInterval = setInterval(() => {
|
|
178
|
-
void this.runHealthCheck(state);
|
|
179
|
-
}, 30000);
|
|
180
|
-
}
|
|
181
|
-
async runHealthCheck(state) {
|
|
182
|
-
if (!state.client || state.status !== 'ready') {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
await state.client.request('workspace/symbol', { query: '__lsp_mcp_healthcheck__' }, 5000);
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
const message = error instanceof Error ? error.message : 'LSP health check failed';
|
|
190
|
-
await this.restartLanguage(state, message);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async restartLanguage(state, reason) {
|
|
194
|
-
if (state.healthInterval) {
|
|
195
|
-
clearInterval(state.healthInterval);
|
|
196
|
-
state.healthInterval = null;
|
|
197
|
-
}
|
|
198
|
-
if (state.restartCount >= 3) {
|
|
199
|
-
state.status = 'error';
|
|
200
|
-
state.error = reason;
|
|
201
|
-
state.client = null;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
state.restartCount += 1;
|
|
205
|
-
state.status = 'starting';
|
|
206
|
-
state.error = reason;
|
|
207
|
-
if (state.client) {
|
|
208
|
-
try {
|
|
209
|
-
await promiseWithTimeout(state.client.shutdown(), 5000, 'LSP shutdown timed out');
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
// Ignore shutdown failures during restart.
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
state.client = null;
|
|
216
|
-
await this.startLanguage(state);
|
|
217
|
-
}
|
|
218
|
-
async resolveServer(language) {
|
|
219
|
-
const available = await (0, lsp_mapping_1.findAvailableLsp)(language);
|
|
220
|
-
if (available) {
|
|
221
|
-
return available;
|
|
222
|
-
}
|
|
223
|
-
const candidate = (0, lsp_mapping_1.getLspCandidates)(language)[0] ?? null;
|
|
224
|
-
if (!candidate) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const installation = await (0, installer_1.installLsp)(candidate);
|
|
228
|
-
if (!installation.success) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return candidate;
|
|
232
|
-
}
|
|
233
|
-
storeDiagnostics(params) {
|
|
234
|
-
if (!isPublishDiagnosticsParams(params)) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
this.diagnostics.set(params.uri, cloneDiagnostics(params.diagnostics));
|
|
238
100
|
}
|
|
239
101
|
}
|
|
240
102
|
exports.LifecycleManager = LifecycleManager;
|
|
@@ -244,20 +106,6 @@ function normalizeLogLevel(level) {
|
|
|
244
106
|
}
|
|
245
107
|
return 'info';
|
|
246
108
|
}
|
|
247
|
-
function isPublishDiagnosticsParams(value) {
|
|
248
|
-
return typeof value === 'object' && value !== null
|
|
249
|
-
&& 'uri' in value && typeof value.uri === 'string'
|
|
250
|
-
&& 'diagnostics' in value && Array.isArray(value.diagnostics);
|
|
251
|
-
}
|
|
252
|
-
function cloneDiagnostics(diagnostics) {
|
|
253
|
-
return diagnostics.map((diagnostic) => ({
|
|
254
|
-
...diagnostic,
|
|
255
|
-
range: {
|
|
256
|
-
start: { ...diagnostic.range.start },
|
|
257
|
-
end: { ...diagnostic.range.end }
|
|
258
|
-
}
|
|
259
|
-
}));
|
|
260
|
-
}
|
|
261
109
|
async function promiseWithTimeout(promise, timeoutMs, message) {
|
|
262
110
|
return await new Promise((resolve, reject) => {
|
|
263
111
|
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|