@theupsider/lsp-mcp 1.1.1 → 1.2.0
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/__tests__/diagnostic-store.test.js +57 -0
- package/dist/lsp/__tests__/lsp-client.test.js +73 -0
- package/dist/lsp/lifecycle-manager.js +31 -4
- package/dist/lsp/lsp-client.js +13 -3
- package/dist/mcp/__tests__/read-tools.test.js +69 -1
- package/dist/mcp/__tests__/write-tools.test.js +2 -2
- package/dist/mcp/tools/read-tools.js +39 -19
- package/package.json +2 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const diagnostic_store_1 = require("../diagnostic-store");
|
|
4
|
+
describe('DiagnosticStore', () => {
|
|
5
|
+
it('replaces diagnostics wholesale per uri on each publish', () => {
|
|
6
|
+
const store = new diagnostic_store_1.DiagnosticStore();
|
|
7
|
+
store.store({ uri: 'file:///a.ts', diagnostics: [makeDiagnostic(1, 'first')] });
|
|
8
|
+
store.store({ uri: 'file:///a.ts', diagnostics: [makeDiagnostic(2, 'second')] });
|
|
9
|
+
expect(store.getForFile('/a.ts')).toEqual([
|
|
10
|
+
{ ...makeDiagnostic(2, 'second'), uri: 'file:///a.ts' },
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
it('ignores malformed publishDiagnostics payloads', () => {
|
|
14
|
+
const store = new diagnostic_store_1.DiagnosticStore();
|
|
15
|
+
store.store(null);
|
|
16
|
+
store.store({ uri: 'file:///a.ts' });
|
|
17
|
+
store.store({ diagnostics: [] });
|
|
18
|
+
expect(store.getForFile('/a.ts')).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it('sorts workspace diagnostics by severity ascending (errors before warnings)', () => {
|
|
21
|
+
const store = new diagnostic_store_1.DiagnosticStore();
|
|
22
|
+
store.store({
|
|
23
|
+
uri: 'file:///warn.ts',
|
|
24
|
+
diagnostics: [makeDiagnostic(2, 'a warning')],
|
|
25
|
+
});
|
|
26
|
+
store.store({
|
|
27
|
+
uri: 'file:///error.ts',
|
|
28
|
+
diagnostics: [makeDiagnostic(1, 'an error')],
|
|
29
|
+
});
|
|
30
|
+
store.store({
|
|
31
|
+
uri: 'file:///hint.ts',
|
|
32
|
+
diagnostics: [makeDiagnostic(4, 'a hint')],
|
|
33
|
+
});
|
|
34
|
+
const result = store.getForWorkspace();
|
|
35
|
+
expect(result.map((d) => d.message)).toEqual([
|
|
36
|
+
'an error',
|
|
37
|
+
'a warning',
|
|
38
|
+
'a hint',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
it('filters workspace diagnostics by language extension', () => {
|
|
42
|
+
const store = new diagnostic_store_1.DiagnosticStore();
|
|
43
|
+
store.store({ uri: 'file:///a.ts', diagnostics: [makeDiagnostic(1, 'ts issue')] });
|
|
44
|
+
store.store({ uri: 'file:///b.py', diagnostics: [makeDiagnostic(1, 'py issue')] });
|
|
45
|
+
const result = store.getForWorkspace('python');
|
|
46
|
+
expect(result).toEqual([
|
|
47
|
+
{ ...makeDiagnostic(1, 'py issue'), uri: 'file:///b.py' },
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
function makeDiagnostic(severity, message) {
|
|
52
|
+
return {
|
|
53
|
+
severity,
|
|
54
|
+
message,
|
|
55
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const node_events_1 = require("node:events");
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
10
|
const node_stream_1 = require("node:stream");
|
|
5
11
|
const lsp_client_1 = require("../lsp-client");
|
|
6
12
|
jest.mock('node:child_process', () => ({
|
|
@@ -208,6 +214,73 @@ describe('LspClient', () => {
|
|
|
208
214
|
child.stdout.write(encodeMessage({ jsonrpc: '2.0', method: 'textDocument/publishDiagnostics', params: { uri: 'file:///x', diagnostics: [] } }));
|
|
209
215
|
await expect(notificationPromise).resolves.toEqual({ method: 'textDocument/publishDiagnostics', params: { uri: 'file:///x', diagnostics: [] } });
|
|
210
216
|
});
|
|
217
|
+
describe('openAllFilesForDiagnostics (workspace scan regressions)', () => {
|
|
218
|
+
let tmpDir;
|
|
219
|
+
beforeEach(async () => {
|
|
220
|
+
tmpDir = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), 'lsp-client-scan-'));
|
|
221
|
+
});
|
|
222
|
+
afterEach(async () => {
|
|
223
|
+
await (0, promises_1.rm)(tmpDir, { recursive: true, force: true });
|
|
224
|
+
});
|
|
225
|
+
// Publish notifications never arrive in this test (no real server), so
|
|
226
|
+
// shorten the wait so these regression tests run fast under real timers
|
|
227
|
+
// instead of racing fake timers against real fs I/O.
|
|
228
|
+
function withShortDiagnosticsWait(client) {
|
|
229
|
+
const original = client.waitForDiagnosticsPublish.bind(client);
|
|
230
|
+
client.waitForDiagnosticsPublish = (filePath) => original(filePath, 50);
|
|
231
|
+
}
|
|
232
|
+
it('does not let one unreadable file abort the whole scan (regression)', async () => {
|
|
233
|
+
const child = new MockChildProcess();
|
|
234
|
+
const spawn = jest.requireMock('node:child_process').spawn;
|
|
235
|
+
spawn.mockReturnValue(child);
|
|
236
|
+
const client = new lsp_client_1.LspClient(SERVER, tmpDir, 'error');
|
|
237
|
+
await startClient(client, child);
|
|
238
|
+
withShortDiagnosticsWait(client);
|
|
239
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(tmpDir, 'a.ts'), 'const a = 1;\n');
|
|
240
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(tmpDir, 'b.ts'), 'const b = 1;\n');
|
|
241
|
+
const brokenPath = node_path_1.default.join(tmpDir, 'broken.ts');
|
|
242
|
+
await (0, promises_1.writeFile)(brokenPath, 'const c = 1;\n');
|
|
243
|
+
await (0, promises_1.chmod)(brokenPath, 0o000);
|
|
244
|
+
if (process.getuid?.() === 0) {
|
|
245
|
+
// Running as root: chmod 0o000 does not block reads, so this test cannot apply.
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const summary = await client.openAllFilesForDiagnostics(['.ts']);
|
|
249
|
+
expect(summary).toEqual({ opened: 2, failed: 1, truncated: false });
|
|
250
|
+
}, 10000);
|
|
251
|
+
it('skips virtualenv-style directories during the workspace scan (regression)', async () => {
|
|
252
|
+
const child = new MockChildProcess();
|
|
253
|
+
const spawn = jest.requireMock('node:child_process').spawn;
|
|
254
|
+
spawn.mockReturnValue(child);
|
|
255
|
+
const client = new lsp_client_1.LspClient(SERVER, tmpDir, 'error');
|
|
256
|
+
await startClient(client, child);
|
|
257
|
+
withShortDiagnosticsWait(client);
|
|
258
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(tmpDir, 'top1.py'), 'x = 1\n');
|
|
259
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(tmpDir, 'top2.py'), 'y = 2\n');
|
|
260
|
+
const venvPkgDir = node_path_1.default.join(tmpDir, '.venv', 'lib', 'site-packages');
|
|
261
|
+
await (0, promises_1.mkdir)(venvPkgDir, { recursive: true });
|
|
262
|
+
await Promise.all(Array.from({ length: 5 }, (_, i) => (0, promises_1.writeFile)(node_path_1.default.join(venvPkgDir, `dep${i}.py`), `dep = ${i}\n`)));
|
|
263
|
+
const summary = await client.openAllFilesForDiagnostics(['.py']);
|
|
264
|
+
expect(summary).toEqual({ opened: 2, failed: 0, truncated: false });
|
|
265
|
+
const openedUris = child.stdinWrites
|
|
266
|
+
.map((raw) => extractMessage(raw))
|
|
267
|
+
.filter((message) => message.method === 'textDocument/didOpen')
|
|
268
|
+
.map((message) => getObject(message.params).textDocument)
|
|
269
|
+
.map((textDocument) => textDocument.uri);
|
|
270
|
+
expect(openedUris.some((uri) => uri.includes('.venv'))).toBe(false);
|
|
271
|
+
}, 10000);
|
|
272
|
+
it('caps the workspace scan at 100 files per language and reports truncation', async () => {
|
|
273
|
+
const child = new MockChildProcess();
|
|
274
|
+
const spawn = jest.requireMock('node:child_process').spawn;
|
|
275
|
+
spawn.mockReturnValue(child);
|
|
276
|
+
const client = new lsp_client_1.LspClient(SERVER, tmpDir, 'error');
|
|
277
|
+
await startClient(client, child);
|
|
278
|
+
withShortDiagnosticsWait(client);
|
|
279
|
+
await Promise.all(Array.from({ length: 150 }, (_, i) => (0, promises_1.writeFile)(node_path_1.default.join(tmpDir, `file${i}.ts`), `const v${i} = ${i};\n`)));
|
|
280
|
+
const summary = await client.openAllFilesForDiagnostics(['.ts']);
|
|
281
|
+
expect(summary).toEqual({ opened: 100, failed: 0, truncated: true });
|
|
282
|
+
}, 10000);
|
|
283
|
+
});
|
|
211
284
|
});
|
|
212
285
|
function encodeMessage(payload) {
|
|
213
286
|
const body = JSON.stringify(payload);
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.LifecycleManager = void 0;
|
|
7
|
+
exports.promiseWithTimeout = promiseWithTimeout;
|
|
7
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
9
|
const language_detector_1 = require("../detection/language-detector");
|
|
9
10
|
const language_registry_1 = require("../detection/language-registry");
|
|
@@ -68,18 +69,44 @@ class LifecycleManager {
|
|
|
68
69
|
return this.store.getForWorkspace(language);
|
|
69
70
|
}
|
|
70
71
|
async analyzeWorkspace(language) {
|
|
71
|
-
await Promise.
|
|
72
|
+
const results = await Promise.allSettled(Array.from(this.supervisors.entries())
|
|
72
73
|
.filter(([lang, supervisor]) => {
|
|
73
74
|
const health = supervisor.getHealth();
|
|
74
75
|
return health.status === "ready" && (!language || lang === language);
|
|
75
76
|
})
|
|
76
77
|
.map(async ([lang, supervisor]) => {
|
|
77
78
|
const client = supervisor.getClient();
|
|
78
|
-
if (!client)
|
|
79
|
-
return;
|
|
79
|
+
if (!client) {
|
|
80
|
+
return { language: lang, opened: 0, failed: 0, truncated: false };
|
|
81
|
+
}
|
|
80
82
|
const extensions = (0, language_registry_1.extensionsForLanguage)(lang);
|
|
81
|
-
|
|
83
|
+
try {
|
|
84
|
+
const scan = await client.openAllFilesForDiagnostics(extensions);
|
|
85
|
+
return { language: lang, ...scan };
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
language: lang,
|
|
90
|
+
opened: 0,
|
|
91
|
+
failed: 0,
|
|
92
|
+
truncated: false,
|
|
93
|
+
error: error instanceof Error ? error.message : String(error),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
82
96
|
}));
|
|
97
|
+
return {
|
|
98
|
+
perLanguage: results.map((result) => result.status === "fulfilled"
|
|
99
|
+
? result.value
|
|
100
|
+
: {
|
|
101
|
+
language: "unknown",
|
|
102
|
+
opened: 0,
|
|
103
|
+
failed: 0,
|
|
104
|
+
truncated: false,
|
|
105
|
+
error: result.reason instanceof Error
|
|
106
|
+
? result.reason.message
|
|
107
|
+
: String(result.reason),
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
83
110
|
}
|
|
84
111
|
async ensureSeedFilesOpen() {
|
|
85
112
|
await Promise.all(Array.from(this.supervisors.entries()).map(async ([language, supervisor]) => {
|
package/dist/lsp/lsp-client.js
CHANGED
|
@@ -176,13 +176,16 @@ class LspClient extends node_events_1.EventEmitter {
|
|
|
176
176
|
const MAX_FILES = 100;
|
|
177
177
|
const allFiles = await findAllFilesWithExtension(this.projectRoot, extensions, MAX_FILES);
|
|
178
178
|
const newFiles = allFiles.filter((f) => !this.openedFiles.has(f));
|
|
179
|
+
const truncated = allFiles.length >= MAX_FILES;
|
|
179
180
|
if (newFiles.length === 0) {
|
|
180
|
-
return;
|
|
181
|
+
return { opened: 0, failed: 0, truncated };
|
|
181
182
|
}
|
|
182
183
|
// Set up wait promises BEFORE opening files to avoid missing notifications
|
|
183
184
|
const waitPromises = newFiles.map((f) => this.waitForDiagnosticsPublish(f, 15000));
|
|
184
|
-
await Promise.
|
|
185
|
-
await Promise.
|
|
185
|
+
const openResults = await Promise.allSettled(newFiles.map((f) => this.ensureDidOpen(f)));
|
|
186
|
+
await Promise.allSettled(waitPromises);
|
|
187
|
+
const failed = openResults.filter((result) => result.status === "rejected").length;
|
|
188
|
+
return { opened: newFiles.length - failed, failed, truncated };
|
|
186
189
|
}
|
|
187
190
|
sendMessage(message) {
|
|
188
191
|
if (!this.process) {
|
|
@@ -311,6 +314,13 @@ const SKIP_DIRS = new Set([
|
|
|
311
314
|
"target",
|
|
312
315
|
".cache",
|
|
313
316
|
"vendor",
|
|
317
|
+
".venv",
|
|
318
|
+
"venv",
|
|
319
|
+
".tox",
|
|
320
|
+
".mypy_cache",
|
|
321
|
+
".pytest_cache",
|
|
322
|
+
".idea",
|
|
323
|
+
".gradle",
|
|
314
324
|
]);
|
|
315
325
|
async function findFirstFileWithExtension(dir, extensions) {
|
|
316
326
|
const extensionSet = new Set(extensions.map((e) => e.toLowerCase()));
|
|
@@ -306,6 +306,74 @@ describe("registerReadTools", () => {
|
|
|
306
306
|
raw: diagnostics,
|
|
307
307
|
});
|
|
308
308
|
});
|
|
309
|
+
it("returns a structured error instead of throwing when workspace analysis fails (regression)", async () => {
|
|
310
|
+
const registrar = new FakeRegistrar();
|
|
311
|
+
const lifecycle = createLifecycle({ diagnostics: [] });
|
|
312
|
+
lifecycle.analyzeWorkspace.mockRejectedValueOnce(new Error("boom"));
|
|
313
|
+
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
314
|
+
await expect(getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" })).resolves.toMatchObject({
|
|
315
|
+
content: [{ type: "text", text: "boom" }],
|
|
316
|
+
error: true,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
it("reports the standard timeout message when workspace analysis times out (regression)", async () => {
|
|
320
|
+
const registrar = new FakeRegistrar();
|
|
321
|
+
const lifecycle = createLifecycle({ diagnostics: [] });
|
|
322
|
+
lifecycle.analyzeWorkspace.mockRejectedValueOnce(new Error("Workspace diagnostics timed out"));
|
|
323
|
+
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
324
|
+
await expect(getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" })).resolves.toEqual({
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: "Operation timed out after 30s — try a more specific query or check the LSP server health",
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
error: true,
|
|
332
|
+
raw: null,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
it("caps workspace diagnostics at 200 results", async () => {
|
|
336
|
+
const registrar = new FakeRegistrar();
|
|
337
|
+
const many = Array.from({ length: 250 }, (_, i) => ({
|
|
338
|
+
uri: `file:///workspace/src/file${i}.ts`,
|
|
339
|
+
message: `issue ${i}`,
|
|
340
|
+
severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Warning,
|
|
341
|
+
range: {
|
|
342
|
+
start: { line: 0, character: 0 },
|
|
343
|
+
end: { line: 0, character: 1 },
|
|
344
|
+
},
|
|
345
|
+
}));
|
|
346
|
+
const lifecycle = createLifecycle({ diagnostics: many });
|
|
347
|
+
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
348
|
+
const result = (await getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" }));
|
|
349
|
+
expect(result.raw).toHaveLength(200);
|
|
350
|
+
expect(result.raw).toEqual(many.slice(0, 200));
|
|
351
|
+
});
|
|
352
|
+
it("notes truncated/errored languages in workspace diagnostics text without altering raw (regression)", async () => {
|
|
353
|
+
const registrar = new FakeRegistrar();
|
|
354
|
+
const diagnostics = [
|
|
355
|
+
{
|
|
356
|
+
uri: "file:///workspace/src/index.ts",
|
|
357
|
+
message: "Boom",
|
|
358
|
+
severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Error,
|
|
359
|
+
range: {
|
|
360
|
+
start: { line: 0, character: 0 },
|
|
361
|
+
end: { line: 0, character: 1 },
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
const lifecycle = createLifecycle({ diagnostics });
|
|
366
|
+
lifecycle.analyzeWorkspace.mockResolvedValueOnce({
|
|
367
|
+
perLanguage: [
|
|
368
|
+
{ language: "python", opened: 90, failed: 3, truncated: true },
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
372
|
+
const result = (await getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" }));
|
|
373
|
+
expect(result.content[0]?.text).toContain("100-file scan limit");
|
|
374
|
+
expect(result.content[0]?.text).toContain("python");
|
|
375
|
+
expect(result.raw).toEqual(diagnostics);
|
|
376
|
+
});
|
|
309
377
|
it("returns health instantly without LSP requests", async () => {
|
|
310
378
|
const registrar = new FakeRegistrar();
|
|
311
379
|
const lifecycle = createLifecycle({
|
|
@@ -461,7 +529,7 @@ function createLifecycle(options) {
|
|
|
461
529
|
getHealth: jest.fn(() => options.health ?? []),
|
|
462
530
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
463
531
|
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
464
|
-
analyzeWorkspace: jest.fn().mockResolvedValue(
|
|
532
|
+
analyzeWorkspace: jest.fn().mockResolvedValue({ perLanguage: [] }),
|
|
465
533
|
};
|
|
466
534
|
}
|
|
467
535
|
function createClient(result) {
|
|
@@ -219,7 +219,7 @@ describe("registerWriteTools", () => {
|
|
|
219
219
|
getHealth: jest.fn(() => []),
|
|
220
220
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
221
221
|
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
222
|
-
analyzeWorkspace: jest.fn().mockResolvedValue(
|
|
222
|
+
analyzeWorkspace: jest.fn().mockResolvedValue({ perLanguage: [] }),
|
|
223
223
|
};
|
|
224
224
|
(0, write_tools_1.registerWriteTools)(registrar, noClientLifecycle);
|
|
225
225
|
await expect(getHandler(registrar, "lsp_rename")({ file: "/workspace/README.md", line: 0, character: 0, newName: "x" })).resolves.toEqual({
|
|
@@ -302,7 +302,7 @@ function createLifecycle(client) {
|
|
|
302
302
|
getHealth: jest.fn(() => []),
|
|
303
303
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
304
304
|
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
305
|
-
analyzeWorkspace: jest.fn().mockResolvedValue(
|
|
305
|
+
analyzeWorkspace: jest.fn().mockResolvedValue({ perLanguage: [] }),
|
|
306
306
|
};
|
|
307
307
|
}
|
|
308
308
|
function createClient(result, capabilities = { renameProvider: true }) {
|
|
@@ -10,6 +10,7 @@ const zod_1 = require("zod");
|
|
|
10
10
|
const formatters_1 = require("../formatters");
|
|
11
11
|
const uri_1 = require("../../utils/uri");
|
|
12
12
|
const shared_1 = require("./shared");
|
|
13
|
+
const lifecycle_manager_1 = require("../../lsp/lifecycle-manager");
|
|
13
14
|
function registerReadTools(registrar, lifecycleManager, options) {
|
|
14
15
|
registrar.registerTool("lsp_init", {
|
|
15
16
|
description: "Initialize LSP for a project root. Pass languages to pre-warm specific servers; omit to auto-detect from project files (lazy startup per file if none found).",
|
|
@@ -86,28 +87,47 @@ function registerReadTools(registrar, lifecycleManager, options) {
|
|
|
86
87
|
}, async (args) => {
|
|
87
88
|
const filePath = typeof args.file === "string" ? args.file : "";
|
|
88
89
|
const scope = args.scope === "workspace" ? "workspace" : "file";
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
const timeoutSeconds = scope === "workspace" ? 30 : 10;
|
|
91
|
+
try {
|
|
92
|
+
if (filePath) {
|
|
93
|
+
await lifecycleManager.ensureLanguageForFile(filePath);
|
|
94
|
+
const client = lifecycleManager.getClientForFile(filePath);
|
|
95
|
+
if (client) {
|
|
96
|
+
const waitPromise = client.waitForDiagnosticsPublish(filePath, 10000);
|
|
97
|
+
await client.ensureDidOpen(filePath);
|
|
98
|
+
client.notify("textDocument/didSave", {
|
|
99
|
+
textDocument: { uri: (0, uri_1.pathToUri)(filePath) },
|
|
100
|
+
});
|
|
101
|
+
await waitPromise;
|
|
102
|
+
}
|
|
99
103
|
}
|
|
104
|
+
if (scope === "workspace") {
|
|
105
|
+
const language = typeof args.language === "string" ? args.language : undefined;
|
|
106
|
+
const summary = await (0, lifecycle_manager_1.promiseWithTimeout)(lifecycleManager.analyzeWorkspace(language), 30000, "Workspace diagnostics timed out");
|
|
107
|
+
const diagnostics = lifecycleManager
|
|
108
|
+
.getWorkspaceDiagnostics(language)
|
|
109
|
+
.slice(0, 200);
|
|
110
|
+
let text = (0, formatters_1.formatDiagnostics)(diagnostics, "workspace");
|
|
111
|
+
const truncated = summary.perLanguage
|
|
112
|
+
.filter((entry) => entry.truncated)
|
|
113
|
+
.map((entry) => entry.language);
|
|
114
|
+
const errored = summary.perLanguage
|
|
115
|
+
.filter((entry) => entry.error)
|
|
116
|
+
.map((entry) => `${entry.language}: ${entry.error}`);
|
|
117
|
+
if (truncated.length > 0) {
|
|
118
|
+
text += `\n\nNote: hit the 100-file scan limit for: ${truncated.join(", ")}. Results may be incomplete — narrow scope with { language }.`;
|
|
119
|
+
}
|
|
120
|
+
if (errored.length > 0) {
|
|
121
|
+
text += `\n\nNote: workspace scan failed for: ${errored.join("; ")}. Other languages were unaffected.`;
|
|
122
|
+
}
|
|
123
|
+
return (0, shared_1.success)(text, diagnostics);
|
|
124
|
+
}
|
|
125
|
+
const diagnostics = lifecycleManager.getFileDiagnostics(filePath);
|
|
126
|
+
return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "file"), diagnostics);
|
|
100
127
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await lifecycleManager.analyzeWorkspace(language);
|
|
104
|
-
const diagnostics = lifecycleManager
|
|
105
|
-
.getWorkspaceDiagnostics(language)
|
|
106
|
-
.slice(0, 200);
|
|
107
|
-
return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "workspace"), diagnostics);
|
|
128
|
+
catch (error) {
|
|
129
|
+
return (0, shared_1.mapToolError)(error, timeoutSeconds);
|
|
108
130
|
}
|
|
109
|
-
const diagnostics = lifecycleManager.getFileDiagnostics(filePath);
|
|
110
|
-
return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "file"), diagnostics);
|
|
111
131
|
});
|
|
112
132
|
registrar.registerTool("lsp_type_definition", { description: "Find type definitions", inputSchema: positionSchema }, async (args) => {
|
|
113
133
|
return await runFileRequest({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theupsider/lsp-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Universal LSP MCP server",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -44,4 +44,4 @@
|
|
|
44
44
|
"ts-jest": "latest",
|
|
45
45
|
"typescript": "latest"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|