@theupsider/lsp-mcp 1.0.0 → 1.0.7
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/lsp/lifecycle-manager.js +22 -7
- package/dist/lsp/lsp-client.js +57 -4
- package/dist/lsp/server-supervisor.js +17 -18
- package/dist/mcp/__tests__/read-tools.test.js +354 -146
- package/dist/mcp/__tests__/write-tools.test.js +218 -104
- package/dist/mcp/formatters.js +109 -91
- package/dist/mcp/server.js +30 -14
- package/dist/mcp/tools/read-tools.js +104 -58
- package/dist/mcp/tools/shared.js +19 -11
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ class LifecycleManager {
|
|
|
20
20
|
}
|
|
21
21
|
async start(languages) {
|
|
22
22
|
const startPromise = this.startInternal(languages);
|
|
23
|
-
await promiseWithTimeout(startPromise, 30000,
|
|
23
|
+
await promiseWithTimeout(startPromise, 30000, "Lifecycle start timed out");
|
|
24
24
|
}
|
|
25
25
|
async ensureLanguage(language) {
|
|
26
26
|
if (this.supervisors.has(language)) {
|
|
@@ -53,7 +53,8 @@ class LifecycleManager {
|
|
|
53
53
|
return Array.from(this.supervisors.values())
|
|
54
54
|
.filter((supervisor) => {
|
|
55
55
|
const health = supervisor.getHealth();
|
|
56
|
-
return health.status ===
|
|
56
|
+
return (health.status === "ready" &&
|
|
57
|
+
(!language || supervisor.language === language));
|
|
57
58
|
})
|
|
58
59
|
.flatMap((supervisor) => {
|
|
59
60
|
const client = supervisor.getClient();
|
|
@@ -66,6 +67,20 @@ class LifecycleManager {
|
|
|
66
67
|
getWorkspaceDiagnostics(language) {
|
|
67
68
|
return this.store.getForWorkspace(language);
|
|
68
69
|
}
|
|
70
|
+
async analyzeWorkspace(language) {
|
|
71
|
+
await Promise.all(Array.from(this.supervisors.entries())
|
|
72
|
+
.filter(([lang, supervisor]) => {
|
|
73
|
+
const health = supervisor.getHealth();
|
|
74
|
+
return health.status === "ready" && (!language || lang === language);
|
|
75
|
+
})
|
|
76
|
+
.map(async ([lang, supervisor]) => {
|
|
77
|
+
const client = supervisor.getClient();
|
|
78
|
+
if (!client)
|
|
79
|
+
return;
|
|
80
|
+
const extensions = (0, language_registry_1.extensionsForLanguage)(lang);
|
|
81
|
+
await client.openAllFilesForDiagnostics(extensions);
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
69
84
|
async ensureSeedFilesOpen() {
|
|
70
85
|
await Promise.all(Array.from(this.supervisors.entries()).map(async ([language, supervisor]) => {
|
|
71
86
|
const client = supervisor.getClient();
|
|
@@ -77,8 +92,8 @@ class LifecycleManager {
|
|
|
77
92
|
}
|
|
78
93
|
async shutdown() {
|
|
79
94
|
const supervisors = Array.from(this.supervisors.values());
|
|
80
|
-
const results = await Promise.allSettled(supervisors.map(async (supervisor) => await promiseWithTimeout(supervisor.shutdown(), 5000,
|
|
81
|
-
const errors = results.filter((result) => result.status ===
|
|
95
|
+
const results = await Promise.allSettled(supervisors.map(async (supervisor) => await promiseWithTimeout(supervisor.shutdown(), 5000, "LSP shutdown timed out")));
|
|
96
|
+
const errors = results.filter((result) => result.status === "rejected").length;
|
|
82
97
|
process.stderr.write(`{"timestamp":"${new Date().toISOString()}","level":"info","event":"Shutdown: ${supervisors.length - errors} LSP-Server beendet, ${errors} Fehler"}\n`);
|
|
83
98
|
}
|
|
84
99
|
async startInternal(languages) {
|
|
@@ -93,7 +108,7 @@ class LifecycleManager {
|
|
|
93
108
|
}
|
|
94
109
|
createSupervisor(language) {
|
|
95
110
|
return new server_supervisor_1.ServerSupervisor(language, this.projectRoot, this.logLevel, (method, params) => {
|
|
96
|
-
if (method ===
|
|
111
|
+
if (method === "textDocument/publishDiagnostics") {
|
|
97
112
|
this.store.store(params);
|
|
98
113
|
}
|
|
99
114
|
});
|
|
@@ -101,10 +116,10 @@ class LifecycleManager {
|
|
|
101
116
|
}
|
|
102
117
|
exports.LifecycleManager = LifecycleManager;
|
|
103
118
|
function normalizeLogLevel(level) {
|
|
104
|
-
if (level ===
|
|
119
|
+
if (level === "error" || level === "debug") {
|
|
105
120
|
return level;
|
|
106
121
|
}
|
|
107
|
-
return
|
|
122
|
+
return "info";
|
|
108
123
|
}
|
|
109
124
|
async function promiseWithTimeout(promise, timeoutMs, message) {
|
|
110
125
|
return await new Promise((resolve, reject) => {
|
package/dist/lsp/lsp-client.js
CHANGED
|
@@ -131,7 +131,10 @@ class LspClient extends node_events_1.EventEmitter {
|
|
|
131
131
|
waitForDiagnosticsPublish(filePath, timeoutMs) {
|
|
132
132
|
const uri = (0, uri_1.pathToUri)(filePath);
|
|
133
133
|
return new Promise((resolve) => {
|
|
134
|
-
const timer = setTimeout(
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
this.off("notification", handler);
|
|
136
|
+
resolve();
|
|
137
|
+
}, timeoutMs);
|
|
135
138
|
const handler = (method, params) => {
|
|
136
139
|
if (method === "textDocument/publishDiagnostics") {
|
|
137
140
|
const p = params;
|
|
@@ -169,6 +172,18 @@ class LspClient extends node_events_1.EventEmitter {
|
|
|
169
172
|
await this.ensureDidOpen(file);
|
|
170
173
|
}
|
|
171
174
|
}
|
|
175
|
+
async openAllFilesForDiagnostics(extensions) {
|
|
176
|
+
const MAX_FILES = 100;
|
|
177
|
+
const allFiles = await findAllFilesWithExtension(this.projectRoot, extensions, MAX_FILES);
|
|
178
|
+
const newFiles = allFiles.filter((f) => !this.openedFiles.has(f));
|
|
179
|
+
if (newFiles.length === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Set up wait promises BEFORE opening files to avoid missing notifications
|
|
183
|
+
const waitPromises = newFiles.map((f) => this.waitForDiagnosticsPublish(f, 15000));
|
|
184
|
+
await Promise.all(newFiles.map((f) => this.ensureDidOpen(f)));
|
|
185
|
+
await Promise.all(waitPromises);
|
|
186
|
+
}
|
|
172
187
|
sendMessage(message) {
|
|
173
188
|
if (!this.process) {
|
|
174
189
|
throw new Error("LSP process is not running");
|
|
@@ -285,8 +300,17 @@ function shouldLog(configuredLevel, messageLevel) {
|
|
|
285
300
|
return order[messageLevel] <= order[configuredLevel];
|
|
286
301
|
}
|
|
287
302
|
const SKIP_DIRS = new Set([
|
|
288
|
-
|
|
289
|
-
|
|
303
|
+
"node_modules",
|
|
304
|
+
".git",
|
|
305
|
+
"dist",
|
|
306
|
+
"build",
|
|
307
|
+
"out",
|
|
308
|
+
".next",
|
|
309
|
+
"coverage",
|
|
310
|
+
"__pycache__",
|
|
311
|
+
"target",
|
|
312
|
+
".cache",
|
|
313
|
+
"vendor",
|
|
290
314
|
]);
|
|
291
315
|
async function findFirstFileWithExtension(dir, extensions) {
|
|
292
316
|
const extensionSet = new Set(extensions.map((e) => e.toLowerCase()));
|
|
@@ -298,7 +322,8 @@ async function findFirstFileWithExtension(dir, extensions) {
|
|
|
298
322
|
return null;
|
|
299
323
|
}
|
|
300
324
|
for (const entry of entries) {
|
|
301
|
-
if (entry.isFile() &&
|
|
325
|
+
if (entry.isFile() &&
|
|
326
|
+
extensionSet.has(node_path_1.default.extname(entry.name).toLowerCase())) {
|
|
302
327
|
return node_path_1.default.join(dir, entry.name);
|
|
303
328
|
}
|
|
304
329
|
}
|
|
@@ -311,3 +336,31 @@ async function findFirstFileWithExtension(dir, extensions) {
|
|
|
311
336
|
}
|
|
312
337
|
return null;
|
|
313
338
|
}
|
|
339
|
+
async function findAllFilesWithExtension(dir, extensions, maxFiles, results = []) {
|
|
340
|
+
if (results.length >= maxFiles)
|
|
341
|
+
return results;
|
|
342
|
+
const extensionSet = new Set(extensions.map((e) => e.toLowerCase()));
|
|
343
|
+
let entries;
|
|
344
|
+
try {
|
|
345
|
+
entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (results.length >= maxFiles)
|
|
352
|
+
break;
|
|
353
|
+
if (entry.isFile() &&
|
|
354
|
+
extensionSet.has(node_path_1.default.extname(entry.name).toLowerCase())) {
|
|
355
|
+
results.push(node_path_1.default.join(dir, entry.name));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
if (results.length >= maxFiles)
|
|
360
|
+
break;
|
|
361
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
362
|
+
await findAllFilesWithExtension(node_path_1.default.join(dir, entry.name), extensions, maxFiles, results);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return results;
|
|
366
|
+
}
|
|
@@ -8,10 +8,9 @@ class ServerSupervisor {
|
|
|
8
8
|
language;
|
|
9
9
|
restartCount = 0;
|
|
10
10
|
client = null;
|
|
11
|
-
status =
|
|
11
|
+
status = "starting";
|
|
12
12
|
error = undefined;
|
|
13
13
|
capabilities = undefined;
|
|
14
|
-
serverDef = null;
|
|
15
14
|
healthInterval = null;
|
|
16
15
|
projectRoot;
|
|
17
16
|
logLevel;
|
|
@@ -26,36 +25,36 @@ class ServerSupervisor {
|
|
|
26
25
|
async start() {
|
|
27
26
|
const serverDef = await this.resolveServer();
|
|
28
27
|
if (!serverDef) {
|
|
29
|
-
this.status =
|
|
28
|
+
this.status = "error";
|
|
30
29
|
this.error = `No LSP server available for ${this.language}`;
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
|
-
this.serverDef = serverDef;
|
|
34
32
|
const client = new lsp_client_1.LspClient(serverDef, this.projectRoot, this.logLevel);
|
|
35
33
|
this.client = client;
|
|
36
|
-
client.on(
|
|
34
|
+
client.on("crash", async () => {
|
|
37
35
|
if (this.shuttingDown) {
|
|
38
36
|
return;
|
|
39
37
|
}
|
|
40
38
|
await this.restart(`LSP server crashed for ${this.language}`);
|
|
41
39
|
});
|
|
42
|
-
client.on(
|
|
43
|
-
this.status =
|
|
44
|
-
this.error = error instanceof Error ? error.message :
|
|
40
|
+
client.on("error", (error) => {
|
|
41
|
+
this.status = "error";
|
|
42
|
+
this.error = error instanceof Error ? error.message : "Unknown LSP error";
|
|
45
43
|
});
|
|
46
|
-
client.on(
|
|
44
|
+
client.on("notification", (method, params) => {
|
|
47
45
|
this.onDiagnostics(method, params);
|
|
48
46
|
});
|
|
49
47
|
try {
|
|
50
48
|
await client.start();
|
|
51
|
-
this.status =
|
|
49
|
+
this.status = "ready";
|
|
52
50
|
this.error = undefined;
|
|
53
51
|
this.capabilities = client.getCapabilities() ?? undefined;
|
|
54
52
|
this.startHealthChecks();
|
|
55
53
|
}
|
|
56
54
|
catch (error) {
|
|
57
|
-
this.status =
|
|
58
|
-
this.error =
|
|
55
|
+
this.status = "error";
|
|
56
|
+
this.error =
|
|
57
|
+
error instanceof Error ? error.message : "Unknown LSP startup error";
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
async restart(reason) {
|
|
@@ -64,17 +63,17 @@ class ServerSupervisor {
|
|
|
64
63
|
this.healthInterval = null;
|
|
65
64
|
}
|
|
66
65
|
if (this.restartCount >= 3) {
|
|
67
|
-
this.status =
|
|
66
|
+
this.status = "error";
|
|
68
67
|
this.error = reason;
|
|
69
68
|
this.client = null;
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
72
71
|
this.restartCount += 1;
|
|
73
|
-
this.status =
|
|
72
|
+
this.status = "starting";
|
|
74
73
|
this.error = reason;
|
|
75
74
|
if (this.client) {
|
|
76
75
|
try {
|
|
77
|
-
await promiseWithTimeout(this.client.shutdown(), 5000,
|
|
76
|
+
await promiseWithTimeout(this.client.shutdown(), 5000, "LSP shutdown timed out");
|
|
78
77
|
}
|
|
79
78
|
catch {
|
|
80
79
|
// Ignore shutdown failures during restart.
|
|
@@ -102,7 +101,7 @@ class ServerSupervisor {
|
|
|
102
101
|
language: this.language,
|
|
103
102
|
status: this.status,
|
|
104
103
|
error: this.error,
|
|
105
|
-
capabilities: this.capabilities
|
|
104
|
+
capabilities: this.capabilities,
|
|
106
105
|
};
|
|
107
106
|
}
|
|
108
107
|
startHealthChecks() {
|
|
@@ -114,11 +113,11 @@ class ServerSupervisor {
|
|
|
114
113
|
}, 30000);
|
|
115
114
|
}
|
|
116
115
|
async runHealthCheck() {
|
|
117
|
-
if (!this.client || this.status !==
|
|
116
|
+
if (!this.client || this.status !== "ready") {
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
119
|
if (!this.client.isReady()) {
|
|
121
|
-
await this.restart(
|
|
120
|
+
await this.restart("LSP client lost ready state");
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
async resolveServer() {
|