@theupsider/lsp-mcp 1.0.7 → 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
  │ ▼ │
@@ -126,22 +126,41 @@ describe("registerReadTools", () => {
126
126
  raw: null,
127
127
  });
128
128
  });
129
- it("sends didOpen once and returns formatted hover results", async () => {
129
+ it("sends didOpen and returns formatted definition results", async () => {
130
130
  const registrar = new FakeRegistrar();
131
- const client = createClient({ contents: "hover docs" });
131
+ const client = createClient({
132
+ uri: "file:///workspace/src/defs.ts",
133
+ range: {
134
+ start: { line: 3, character: 1 },
135
+ end: { line: 3, character: 2 },
136
+ },
137
+ });
132
138
  const lifecycle = createLifecycle({ fileClient: client });
133
139
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
134
- const hover = await getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
135
- await getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
140
+ const definition = await getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
141
+ await getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
136
142
  expect(client.ensureDidOpen).toHaveBeenCalledTimes(2);
137
143
  expect(client.ensureDidOpen).toHaveBeenCalledWith("/workspace/src/index.ts");
138
- expect(client.request).toHaveBeenCalledWith("textDocument/hover", {
144
+ expect(client.request).toHaveBeenCalledWith("textDocument/definition", {
139
145
  textDocument: { uri: "file:///workspace/src/index.ts" },
140
146
  position: { line: 2, character: 4 },
141
147
  }, 5000);
142
- expect(hover).toEqual({
143
- content: [{ type: "text", text: "hover docs" }],
144
- raw: { contents: "hover docs" },
148
+ expect(definition).toEqual({
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
153
+ },
154
+ ],
155
+ raw: [
156
+ {
157
+ path: "/workspace/src/defs.ts",
158
+ range: {
159
+ start: { line: 3, character: 1 },
160
+ end: { line: 3, character: 2 },
161
+ },
162
+ },
163
+ ],
145
164
  });
146
165
  });
147
166
  it("formats definitions and converts URIs back to paths", async () => {
@@ -303,22 +322,9 @@ describe("registerReadTools", () => {
303
322
  raw: [{ language: "typescript", status: "ready" }],
304
323
  });
305
324
  });
306
- it("supports document symbols, completion lists, and signature help fallbacks", async () => {
325
+ it("supports document symbols", async () => {
307
326
  const registrar = new FakeRegistrar();
308
- const client = createClient({ items: [{ label: "x", kind: 3 }] });
309
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
310
- initializeManager: jest.fn(),
311
- });
312
- await expect(getHandler(registrar, "lsp_completion")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
313
- content: [
314
- {
315
- type: "text",
316
- text: "Showing 1 of 1 completion item(s)\n\n### Functions\n- `x`",
317
- },
318
- ],
319
- raw: [{ label: "x", kind: 3 }],
320
- });
321
- client.request.mockResolvedValueOnce([
327
+ const client = createClient([
322
328
  {
323
329
  name: "DocSymbol",
324
330
  kind: 5,
@@ -332,6 +338,9 @@ describe("registerReadTools", () => {
332
338
  },
333
339
  },
334
340
  ]);
341
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
342
+ initializeManager: jest.fn(),
343
+ });
335
344
  await expect(getHandler(registrar, "lsp_document_symbols")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
336
345
  content: [{ type: "text", text: "- 📦 `DocSymbol`" }],
337
346
  raw: [
@@ -349,13 +358,8 @@ describe("registerReadTools", () => {
349
358
  },
350
359
  ],
351
360
  });
352
- client.request.mockResolvedValueOnce(null);
353
- await expect(getHandler(registrar, "lsp_signature_help")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
354
- content: [{ type: "text", text: "No result" }],
355
- raw: null,
356
- });
357
361
  });
358
- it("supports type and implementation lookups plus empty completion results", async () => {
362
+ it("supports type and implementation lookups", async () => {
359
363
  const registrar = new FakeRegistrar();
360
364
  const client = createClient({
361
365
  uri: "file:///workspace/src/types.ts",
@@ -389,31 +393,14 @@ describe("registerReadTools", () => {
389
393
  content: [{ type: "text", text: "No result" }],
390
394
  raw: null,
391
395
  });
392
- client.request.mockResolvedValueOnce(null);
393
- await expect(getHandler(registrar, "lsp_completion")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
394
- content: [{ type: "text", text: "No result" }],
395
- raw: null,
396
- });
397
- client.request.mockResolvedValueOnce({
398
- signatures: [{ label: "fn(x: string)" }],
399
- });
400
- await expect(getHandler(registrar, "lsp_signature_help")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
401
- content: [
402
- {
403
- type: "text",
404
- text: JSON.stringify({ signatures: [{ label: "fn(x: string)" }] }, null, 2),
405
- },
406
- ],
407
- raw: { signatures: [{ label: "fn(x: string)" }] },
408
- });
409
396
  });
410
397
  it("turns LSP timeouts into retry guidance", async () => {
411
398
  const registrar = new FakeRegistrar();
412
- const client = createClient(new Error("LSP request timed out: textDocument/hover"));
399
+ const client = createClient(new Error("LSP request timed out: textDocument/definition"));
413
400
  (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
414
401
  initializeManager: jest.fn(),
415
402
  });
416
- await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
403
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
417
404
  content: [
418
405
  {
419
406
  type: "text",
@@ -429,7 +416,7 @@ describe("registerReadTools", () => {
429
416
  (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
430
417
  initializeManager: jest.fn(),
431
418
  });
432
- await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
419
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
433
420
  content: [
434
421
  {
435
422
  type: "text",
@@ -446,7 +433,7 @@ describe("registerReadTools", () => {
446
433
  (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
447
434
  initializeManager: jest.fn(),
448
435
  });
449
- await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
436
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
450
437
  content: [
451
438
  {
452
439
  type: "text",
@@ -46,7 +46,7 @@ describe('McpServer', () => {
46
46
  const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
47
47
  readTools.mockImplementationOnce((registrar) => {
48
48
  registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
49
- registrar.registerTool('lsp_hover', { description: 'hover' }, async () => ({ content: [{ type: 'text', text: 'hover' }], raw: null }));
49
+ registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'definition' }], raw: null }));
50
50
  });
51
51
  writeTools.mockImplementationOnce(() => undefined);
52
52
  const { ListToolsRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
@@ -55,14 +55,14 @@ describe('McpServer', () => {
55
55
  const listHandler = getHandler(ListToolsRequestSchema);
56
56
  const result = await listHandler({});
57
57
  expect(result.tools.map((t) => t.name)).toContain('lsp_init');
58
- expect(result.tools.map((t) => t.name)).toContain('lsp_hover');
58
+ expect(result.tools.map((t) => t.name)).toContain('lsp_definition');
59
59
  });
60
60
  it('lists all tools after initialization too', async () => {
61
61
  const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
62
62
  const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
63
63
  readTools.mockImplementationOnce((registrar) => {
64
64
  registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
65
- registrar.registerTool('lsp_hover', { description: 'hover' }, async () => ({ content: [{ type: 'text', text: 'hover' }], raw: null }));
65
+ registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'definition' }], raw: null }));
66
66
  });
67
67
  writeTools.mockImplementationOnce(() => undefined);
68
68
  const { ListToolsRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
@@ -72,20 +72,20 @@ describe('McpServer', () => {
72
72
  const listHandler = getHandler(ListToolsRequestSchema);
73
73
  const result = await listHandler({});
74
74
  expect(result.tools.map((t) => t.name)).toContain('lsp_init');
75
- expect(result.tools.map((t) => t.name)).toContain('lsp_hover');
75
+ expect(result.tools.map((t) => t.name)).toContain('lsp_definition');
76
76
  });
77
77
  it('returns no-root error for non-init tools before lsp_init', async () => {
78
78
  const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
79
79
  const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
80
80
  readTools.mockImplementationOnce((registrar) => {
81
- registrar.registerTool('lsp_hover', { description: 'hover' }, async () => ({ content: [{ type: 'text', text: 'reachable' }], raw: null }));
81
+ registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'reachable' }], raw: null }));
82
82
  });
83
83
  writeTools.mockImplementationOnce(() => undefined);
84
84
  const { CallToolRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
85
85
  const server = new server_1.McpServer('info');
86
86
  await server.start();
87
87
  const callHandler = getHandler(CallToolRequestSchema);
88
- const result = await callHandler({ params: { name: 'lsp_hover', arguments: {} } });
88
+ const result = await callHandler({ params: { name: 'lsp_definition', arguments: {} } });
89
89
  expect(result.content[0].text).toMatch(/No project root set/);
90
90
  });
91
91
  it('re-initializes with new root when lsp_init called again', async () => {
@@ -171,29 +171,7 @@ describe("registerWriteTools", () => {
171
171
  options: { tabSize: 4, insertSpaces: false },
172
172
  }, 15000);
173
173
  });
174
- it("applies a raw workspace edit across files", async () => {
175
- const registrar = new FakeRegistrar();
176
- const client = createClient(null);
177
- (0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
178
- const edit = {
179
- changes: {
180
- "file:///workspace/src/index.ts": [
181
- {
182
- range: {
183
- start: { line: 0, character: 6 },
184
- end: { line: 0, character: 9 },
185
- },
186
- newText: "bar",
187
- },
188
- ],
189
- },
190
- };
191
- await expect(getHandler(registrar, "lsp_apply_workspace_edit")({ edit })).resolves.toEqual({
192
- content: [{ type: "text", text: "Applied workspace edit to 1 file(s)" }],
193
- raw: { changedFiles: ["/workspace/src/index.ts"] },
194
- });
195
- });
196
- it("supports indexed code actions, empty edits, and document changes", async () => {
174
+ it("supports indexed code actions and document changes", async () => {
197
175
  const registrar = new FakeRegistrar();
198
176
  const client = createClient([
199
177
  { title: "Skip me" },
@@ -230,34 +208,6 @@ describe("registerWriteTools", () => {
230
208
  content: [{ type: "text", text: "Applied code action: Apply me" }],
231
209
  raw: { title: "Apply me", changedFiles: ["/workspace/src/index.ts"] },
232
210
  });
233
- client.request.mockResolvedValueOnce(null);
234
- await expect(getHandler(registrar, "lsp_apply_workspace_edit")({ edit: null })).resolves.toEqual({
235
- content: [{ type: "text", text: "No result" }],
236
- raw: null,
237
- });
238
- });
239
- it("returns an error when no write-capable client is ready", async () => {
240
- const registrar = new FakeRegistrar();
241
- (0, write_tools_1.registerWriteTools)(registrar, {
242
- getClientForFile: jest.fn(() => null),
243
- getReadyClients: jest.fn(() => []),
244
- getFileDiagnostics: jest.fn((_) => []),
245
- getWorkspaceDiagnostics: jest.fn(() => []),
246
- getHealth: jest.fn(() => []),
247
- ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
248
- ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
249
- analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
250
- });
251
- await expect(getHandler(registrar, "lsp_apply_workspace_edit")({ edit: {} })).resolves.toEqual({
252
- content: [
253
- {
254
- type: "text",
255
- text: "No language servers are ready. Run lsp_health for details.",
256
- },
257
- ],
258
- error: true,
259
- raw: null,
260
- });
261
211
  });
262
212
  it("handles missing clients, formatter defaults, and empty code actions", async () => {
263
213
  const registrar = new FakeRegistrar();
@@ -20,16 +20,6 @@ function registerReadTools(registrar, lifecycleManager, options) {
20
20
  }, async (args) => {
21
21
  return await handleInitTool(args, options);
22
22
  });
23
- registrar.registerTool("lsp_hover", { description: "Show hover information", inputSchema: positionSchema }, async (args) => {
24
- return await runFileRequest({
25
- args,
26
- lifecycleManager,
27
- method: "textDocument/hover",
28
- timeoutMs: 5000,
29
- format: formatters_1.formatHover,
30
- raw: (result) => result,
31
- });
32
- });
33
23
  registrar.registerTool("lsp_definition", { description: "Find definitions", inputSchema: positionSchema }, async (args) => {
34
24
  return await runFileRequest({
35
25
  args,
@@ -86,16 +76,6 @@ function registerReadTools(registrar, lifecycleManager, options) {
86
76
  const merged = results.flat().slice(0, query === "" ? 100 : 500);
87
77
  return (0, shared_1.success)((0, formatters_1.formatSymbols)(merged), (0, shared_1.normalizeSymbols)(merged));
88
78
  });
89
- registrar.registerTool("lsp_completion", { description: "Get completions", inputSchema: positionSchema }, async (args) => {
90
- return await runFileRequest({
91
- args,
92
- lifecycleManager,
93
- method: "textDocument/completion",
94
- timeoutMs: 5000,
95
- format: (result) => (0, formatters_1.formatCompletion)(asCompletionItems(result)),
96
- raw: (result) => asCompletionItems(result)?.slice(0, 50) ?? null,
97
- });
98
- });
99
79
  registrar.registerTool("lsp_diagnostics", {
100
80
  description: "Get errors and warnings. Pass a file path to trigger analysis of that file and its language server, then return diagnostics for that file (scope: file) or all files seen so far (scope: workspace). Omit file for workspace scope to query whatever has been opened previously.",
101
81
  inputSchema: zod_1.z.object({
@@ -129,16 +109,6 @@ function registerReadTools(registrar, lifecycleManager, options) {
129
109
  const diagnostics = lifecycleManager.getFileDiagnostics(filePath);
130
110
  return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "file"), diagnostics);
131
111
  });
132
- registrar.registerTool("lsp_signature_help", { description: "Show signature help", inputSchema: positionSchema }, async (args) => {
133
- return await runFileRequest({
134
- args,
135
- lifecycleManager,
136
- method: "textDocument/signatureHelp",
137
- timeoutMs: 5000,
138
- format: stringifyResult,
139
- raw: (result) => result,
140
- });
141
- });
142
112
  registrar.registerTool("lsp_type_definition", { description: "Find type definitions", inputSchema: positionSchema }, async (args) => {
143
113
  return await runFileRequest({
144
114
  args,
@@ -242,18 +212,6 @@ function asLocationArray(result) {
242
212
  }
243
213
  return Array.isArray(result) ? result : [result];
244
214
  }
245
- function asCompletionItems(result) {
246
- if (!result) {
247
- return null;
248
- }
249
- return Array.isArray(result) ? result : result.items;
250
- }
251
- function stringifyResult(result) {
252
- if (!result) {
253
- return "No result";
254
- }
255
- return JSON.stringify(result, null, 2);
256
- }
257
215
  function formatLanguageList(languages) {
258
216
  return languages.length === 0 ? "None" : languages.join(", ");
259
217
  }
@@ -83,19 +83,6 @@ function registerWriteTools(registrar, lifecycleManager) {
83
83
  }, 15000);
84
84
  });
85
85
  });
86
- registrar.registerTool('lsp_apply_workspace_edit', { description: 'Apply raw workspace edit', inputSchema: zod_1.z.object({ edit: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) }) }, async (args) => {
87
- const clients = lifecycleManager.getReadyClients();
88
- const client = clients[0] ?? null;
89
- if (!client) {
90
- return (0, shared_1.failure)('No language servers are ready. Run lsp_health for details.');
91
- }
92
- try {
93
- return await applyWorkspaceEdit((args.edit ?? null), lifecycleManager, client, 'Applied workspace edit to');
94
- }
95
- catch (error) {
96
- return (0, shared_1.mapToolError)(error, 15);
97
- }
98
- });
99
86
  }
100
87
  const positionSchema = zod_1.z.object({ line: zod_1.z.number().int(), character: zod_1.z.number().int() });
101
88
  const rangeSchema = zod_1.z.object({ start: positionSchema, end: positionSchema });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theupsider/lsp-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Universal LSP MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",