@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 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
- - **🛠 19 LSP Tools** — Hover, definition, references, completions, diagnostics, rename, code actions, formatting, and more
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
- │ │ lsp_hover │ │ lsp_... │ │ lsp_... │ │
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, 'Lifecycle start timed out');
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 === 'ready' && (!language || supervisor.language === language);
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, 'LSP shutdown timed out')));
81
- const errors = results.filter((result) => result.status === 'rejected').length;
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 === 'textDocument/publishDiagnostics') {
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 === 'error' || level === 'debug') {
119
+ if (level === "error" || level === "debug") {
105
120
  return level;
106
121
  }
107
- return 'info';
122
+ return "info";
108
123
  }
109
124
  async function promiseWithTimeout(promise, timeoutMs, message) {
110
125
  return await new Promise((resolve, reject) => {
@@ -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(resolve, timeoutMs);
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
- 'node_modules', '.git', 'dist', 'build', 'out', '.next',
289
- 'coverage', '__pycache__', 'target', '.cache', 'vendor',
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() && extensionSet.has(node_path_1.default.extname(entry.name).toLowerCase())) {
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 = 'starting';
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 = 'error';
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('crash', async () => {
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('error', (error) => {
43
- this.status = 'error';
44
- this.error = error instanceof Error ? error.message : 'Unknown LSP error';
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('notification', (method, params) => {
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 = 'ready';
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 = 'error';
58
- this.error = error instanceof Error ? error.message : 'Unknown LSP startup 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 = 'error';
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 = 'starting';
72
+ this.status = "starting";
74
73
  this.error = reason;
75
74
  if (this.client) {
76
75
  try {
77
- await promiseWithTimeout(this.client.shutdown(), 5000, 'LSP shutdown timed out');
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 !== 'ready') {
116
+ if (!this.client || this.status !== "ready") {
118
117
  return;
119
118
  }
120
119
  if (!this.client.isReady()) {
121
- await this.restart('LSP client lost ready state');
120
+ await this.restart("LSP client lost ready state");
122
121
  }
123
122
  }
124
123
  async resolveServer() {