@theupsider/lsp-mcp 0.1.1 → 0.1.4
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/README.md +3 -2
- package/dist/__tests__/index.test.js +5 -7
- package/dist/detection/language-detector.js +7 -8
- package/dist/detection/language-registry.js +38 -0
- package/dist/index.js +1 -16
- 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 +6 -2
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ A Model Context Protocol (MCP) server that gives language models access to **Lan
|
|
|
41
41
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
44
|
+
> **Important:** Install `lsp-mcp` on the **same machine where your code lives**. The language servers it manages need direct filesystem access to your codebase — they cannot work over a remote connection or on a different machine than your source files.
|
|
45
|
+
|
|
44
46
|
```bash
|
|
45
47
|
# npm
|
|
46
48
|
npm install -g @theupsider/lsp-mcp
|
|
@@ -107,7 +109,6 @@ lsp_init({ root: "/path/to/project", languages: ["python", "typescript"] })
|
|
|
107
109
|
|
|
108
110
|
| Environment Variable | Description | Default |
|
|
109
111
|
| -------------------- | ----------------------------------- | ------------ |
|
|
110
|
-
| `LSP_MCP_ROOT` | Project root (auto-calls `lsp_init` on startup) | _(optional)_ |
|
|
111
112
|
| `LSP_MCP_LOG_LEVEL` | Log level: `error`, `info`, `debug` | `info` |
|
|
112
113
|
|
|
113
114
|
## Setup Scripts
|
|
@@ -152,7 +153,7 @@ gem install solargraph
|
|
|
152
153
|
|
|
153
154
|
### Server not detecting language
|
|
154
155
|
|
|
155
|
-
Ensure your project root contains a language marker file (e.g., `package.json` for TypeScript, `Cargo.toml` for Rust). The server scans the `
|
|
156
|
+
Ensure your project root contains a language marker file (e.g., `package.json` for TypeScript, `Cargo.toml` for Rust). The server scans the directory passed to `lsp_init` for these markers.
|
|
156
157
|
|
|
157
158
|
### High memory usage
|
|
158
159
|
|
|
@@ -12,7 +12,7 @@ describe('index entrypoint', () => {
|
|
|
12
12
|
beforeEach(() => {
|
|
13
13
|
jest.clearAllMocks();
|
|
14
14
|
});
|
|
15
|
-
it('starts and waits for lsp_init
|
|
15
|
+
it('starts and waits for lsp_init', async () => {
|
|
16
16
|
const stderr = jest.fn();
|
|
17
17
|
const exit = jest.fn();
|
|
18
18
|
const McpServer = jest.requireMock('../mcp/server').McpServer;
|
|
@@ -26,19 +26,17 @@ 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
|
-
it('
|
|
34
|
+
it('uses LSP_MCP_LOG_LEVEL and registers signals', async () => {
|
|
34
35
|
const stderr = jest.fn();
|
|
35
36
|
const onSignal = jest.fn();
|
|
36
37
|
const McpServer = jest.requireMock('../mcp/server').McpServer;
|
|
37
|
-
await (0, index_1.main)([], {
|
|
38
|
-
const serverInstance = McpServer.mock.results[0]?.value;
|
|
38
|
+
await (0, index_1.main)([], { LSP_MCP_LOG_LEVEL: 'debug' }, { stderr, onSignal });
|
|
39
39
|
expect(McpServer).toHaveBeenCalledWith('debug');
|
|
40
|
-
expect(serverInstance.initializeManager).toHaveBeenCalledWith('/workspace');
|
|
41
|
-
expect(stderr).toHaveBeenCalledWith(`${JSON.stringify({ languages: ['typescript'], started: ['typescript'], errors: [] })}\n`);
|
|
42
40
|
expect(McpServer).toHaveBeenCalledTimes(1);
|
|
43
41
|
expect(onSignal).toHaveBeenCalledTimes(2);
|
|
44
42
|
});
|
|
@@ -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));
|
package/dist/index.js
CHANGED
|
@@ -22,17 +22,9 @@ async function main(argv = process.argv.slice(2), env = process.env, overrides =
|
|
|
22
22
|
exit(0);
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
const projectRoot = env.LSP_MCP_ROOT;
|
|
26
25
|
const logLevel = env.LSP_MCP_LOG_LEVEL ?? 'info';
|
|
27
26
|
const mcpServer = new server_1.McpServer(logLevel);
|
|
28
|
-
|
|
29
|
-
const initialized = await mcpServer.initializeManager(projectRoot);
|
|
30
|
-
const startupReport = summarizeHealth(initialized.health);
|
|
31
|
-
stderr(`${JSON.stringify(startupReport)}\n`);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
stderr(`${JSON.stringify({ event: 'startup', status: 'waiting-for-init' })}\n`);
|
|
35
|
-
}
|
|
27
|
+
stderr(`${JSON.stringify({ event: 'startup', status: 'waiting-for-init' })}\n`);
|
|
36
28
|
await mcpServer.start();
|
|
37
29
|
const shutdown = async () => {
|
|
38
30
|
await mcpServer.shutdown();
|
|
@@ -41,13 +33,6 @@ async function main(argv = process.argv.slice(2), env = process.env, overrides =
|
|
|
41
33
|
onSignal('SIGINT', shutdown);
|
|
42
34
|
onSignal('SIGTERM', shutdown);
|
|
43
35
|
}
|
|
44
|
-
function summarizeHealth(health) {
|
|
45
|
-
return {
|
|
46
|
-
languages: health.map((entry) => entry.language),
|
|
47
|
-
started: health.filter((entry) => entry.status === 'ready').map((entry) => entry.language),
|
|
48
|
-
errors: health.filter((entry) => entry.status === 'error' && entry.error).map((entry) => entry.error)
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
36
|
function readVersion() {
|
|
52
37
|
const packageJsonPath = node_path_1.default.resolve(__dirname, '..', 'package.json');
|
|
53
38
|
return JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf8')).version;
|
|
@@ -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);
|