@theupsider/lsp-mcp 1.0.1 → 1.0.8
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 +2 -6
- 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 +346 -151
- package/dist/mcp/__tests__/server.test.js +6 -6
- package/dist/mcp/__tests__/write-tools.test.js +190 -126
- package/dist/mcp/formatters.js +109 -91
- package/dist/mcp/server.js +30 -14
- package/dist/mcp/tools/read-tools.js +82 -78
- package/dist/mcp/tools/shared.js +19 -11
- package/dist/mcp/tools/write-tools.js +0 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ A Model Context Protocol (MCP) server that gives language models access to **Lan
|
|
|
32
32
|
|
|
33
33
|
- **🔍 Automatic Language Detection** — Scans project root for language markers (`package.json`, `Cargo.toml`, `go.mod`, etc.)
|
|
34
34
|
- **🔄 Auto Language Server Selection** — Hardcoded mapping with fallback servers; installs missing LSPs automatically
|
|
35
|
-
- **🛠
|
|
35
|
+
- **🛠 12 LSP Tools** — Definition, references, symbols, diagnostics, rename, code actions, formatting, and more
|
|
36
36
|
- **📝 Read & Write Operations** — Both inspection and modification of code via LSP
|
|
37
37
|
- **🌐 Polyglot Support** — Multiple language servers run simultaneously in the same project
|
|
38
38
|
- **📋 Hybrid Responses** — Human-readable `text` field + raw LSP data in `raw` field
|
|
@@ -103,14 +103,11 @@ lsp_init({ root: "/path/to/project", languages: ["python", "typescript"] })
|
|
|
103
103
|
| Tool | Description | Key Parameters |
|
|
104
104
|
| ----------------------- | --------------------------------------- | ------------------------------------------------------ |
|
|
105
105
|
| `lsp_init` | Initialize server for a project root | `root` (required), `languages` (optional string array) |
|
|
106
|
-
| `lsp_hover` | Show type info / documentation | `file`, `line`, `character` |
|
|
107
106
|
| `lsp_definition` | Go to definition | `file`, `line`, `character` |
|
|
108
107
|
| `lsp_references` | Find all references | `file`, `line`, `character` |
|
|
109
108
|
| `lsp_document_symbols` | List symbols in a file | `file` |
|
|
110
109
|
| `lsp_workspace_symbols` | Search symbols across workspace | `query` (limit: 100–500 results) |
|
|
111
|
-
| `lsp_completion` | Code completion suggestions | `file`, `line`, `character` |
|
|
112
110
|
| `lsp_diagnostics` | Get errors & warnings | `file` (scope: `file` or `workspace`) |
|
|
113
|
-
| `lsp_signature_help` | Function signature help | `file`, `line`, `character` |
|
|
114
111
|
| `lsp_type_definition` | Go to type definition | `file`, `line`, `character` |
|
|
115
112
|
| `lsp_implementation` | Find implementations | `file`, `line`, `character` |
|
|
116
113
|
| `lsp_health` | Check status of all LSP servers | _(none)_ |
|
|
@@ -123,7 +120,6 @@ lsp_init({ root: "/path/to/project", languages: ["python", "typescript"] })
|
|
|
123
120
|
| `lsp_code_action` | Apply / list code actions | `file`, `line`, `character`, `apply` |
|
|
124
121
|
| `lsp_formatting` | Format document | `file` |
|
|
125
122
|
| `lsp_range_formatting` | Format code range | `file`, `range` |
|
|
126
|
-
| `lsp_apply_workspace_edit` | Apply raw workspace edit | `edit` (WorkspaceEdit object) |
|
|
127
123
|
|
|
128
124
|
## Configuration
|
|
129
125
|
|
|
@@ -190,7 +186,7 @@ Each language server runs as a separate process. For large projects with many la
|
|
|
190
186
|
┌─────────────────────────────────────────────┐
|
|
191
187
|
│ LSP MCP Server (Node.js) │
|
|
192
188
|
│ ┌───────────┐ ┌──────────┐ ┌───────────┐ │
|
|
193
|
-
│ │
|
|
189
|
+
│ │ lsp_init │ │ lsp_... │ │ lsp_... │ │
|
|
194
190
|
│ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │
|
|
195
191
|
│ └─────────────┼─────────────┘ │
|
|
196
192
|
│ ▼ │
|
|
@@ -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() {
|