@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.
@@ -11,119 +11,133 @@ const formatters_1 = require("../formatters");
11
11
  const uri_1 = require("../../utils/uri");
12
12
  const shared_1 = require("./shared");
13
13
  function registerReadTools(registrar, lifecycleManager, options) {
14
- registrar.registerTool('lsp_init', {
15
- 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).',
16
- inputSchema: zod_1.z.object({ root: zod_1.z.string(), languages: zod_1.z.array(zod_1.z.string()).optional() })
14
+ registrar.registerTool("lsp_init", {
15
+ 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).",
16
+ inputSchema: zod_1.z.object({
17
+ root: zod_1.z.string(),
18
+ languages: zod_1.z.array(zod_1.z.string()).optional(),
19
+ }),
17
20
  }, async (args) => {
18
21
  return await handleInitTool(args, options);
19
22
  });
20
- registrar.registerTool('lsp_hover', { description: 'Show hover information', inputSchema: positionSchema }, async (args) => {
21
- return await runFileRequest({ args, lifecycleManager, method: 'textDocument/hover', timeoutMs: 5000, format: formatters_1.formatHover, raw: (result) => result });
22
- });
23
- registrar.registerTool('lsp_definition', { description: 'Find definitions', inputSchema: positionSchema }, async (args) => {
23
+ registrar.registerTool("lsp_definition", { description: "Find definitions", inputSchema: positionSchema }, async (args) => {
24
24
  return await runFileRequest({
25
25
  args,
26
26
  lifecycleManager,
27
- method: 'textDocument/definition',
27
+ method: "textDocument/definition",
28
28
  timeoutMs: 5000,
29
29
  format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
30
- raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
30
+ raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result)),
31
31
  });
32
32
  });
33
- registrar.registerTool('lsp_references', { description: 'Find references', inputSchema: positionSchema.extend({ includeDeclaration: zod_1.z.boolean().optional() }) }, async (args) => {
33
+ registrar.registerTool("lsp_references", {
34
+ description: "Find references",
35
+ inputSchema: positionSchema.extend({
36
+ includeDeclaration: zod_1.z.boolean().optional(),
37
+ }),
38
+ }, async (args) => {
34
39
  const includeDeclaration = args.includeDeclaration === true;
35
40
  return await runFileRequest({
36
41
  args,
37
42
  lifecycleManager,
38
- method: 'textDocument/references',
43
+ method: "textDocument/references",
39
44
  timeoutMs: 15000,
40
- params: (uri, position) => ({ textDocument: { uri }, position, context: { includeDeclaration } }),
45
+ params: (uri, position) => ({
46
+ textDocument: { uri },
47
+ position,
48
+ context: { includeDeclaration },
49
+ }),
41
50
  format: formatters_1.formatReferences,
42
- raw: shared_1.normalizeLocations
51
+ raw: shared_1.normalizeLocations,
43
52
  });
44
53
  });
45
- registrar.registerTool('lsp_document_symbols', { description: 'List document symbols', inputSchema: zod_1.z.object({ file: zod_1.z.string() }) }, async (args) => {
54
+ registrar.registerTool("lsp_document_symbols", {
55
+ description: "List document symbols",
56
+ inputSchema: zod_1.z.object({ file: zod_1.z.string() }),
57
+ }, async (args) => {
46
58
  return await runFileRequest({
47
59
  args,
48
60
  lifecycleManager,
49
- method: 'textDocument/documentSymbol',
61
+ method: "textDocument/documentSymbol",
50
62
  timeoutMs: 15000,
51
63
  format: formatters_1.formatSymbols,
52
- raw: shared_1.normalizeSymbols
64
+ raw: shared_1.normalizeSymbols,
53
65
  });
54
66
  });
55
- registrar.registerTool('lsp_workspace_symbols', { description: 'Search workspace symbols', inputSchema: zod_1.z.object({ query: zod_1.z.string().default('') }) }, async (args) => {
56
- const query = typeof args.query === 'string' ? args.query : '';
67
+ registrar.registerTool("lsp_workspace_symbols", {
68
+ description: "Search workspace symbols",
69
+ inputSchema: zod_1.z.object({ query: zod_1.z.string().default("") }),
70
+ }, async (args) => {
71
+ const query = typeof args.query === "string" ? args.query : "";
57
72
  await lifecycleManager.ensureSeedFilesOpen();
58
73
  const results = await Promise.all(lifecycleManager.getReadyClients().map(async (client) => {
59
- return await client.request('workspace/symbol', { query }, 30000);
74
+ return (await client.request("workspace/symbol", { query }, 30000));
60
75
  }));
61
- const merged = results.flat().slice(0, query === '' ? 100 : 500);
76
+ const merged = results.flat().slice(0, query === "" ? 100 : 500);
62
77
  return (0, shared_1.success)((0, formatters_1.formatSymbols)(merged), (0, shared_1.normalizeSymbols)(merged));
63
78
  });
64
- registrar.registerTool('lsp_completion', { description: 'Get completions', inputSchema: positionSchema }, async (args) => {
65
- return await runFileRequest({
66
- args,
67
- lifecycleManager,
68
- method: 'textDocument/completion',
69
- timeoutMs: 5000,
70
- format: (result) => (0, formatters_1.formatCompletion)(asCompletionItems(result)),
71
- raw: (result) => asCompletionItems(result)?.slice(0, 50) ?? null
72
- });
73
- });
74
- registrar.registerTool('lsp_diagnostics', { 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.', inputSchema: zod_1.z.object({ file: zod_1.z.string().optional(), scope: zod_1.z.enum(['file', 'workspace']).default('file'), language: zod_1.z.string().optional() }) }, async (args) => {
75
- const filePath = typeof args.file === 'string' ? args.file : '';
76
- const scope = args.scope === 'workspace' ? 'workspace' : 'file';
79
+ registrar.registerTool("lsp_diagnostics", {
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.",
81
+ inputSchema: zod_1.z.object({
82
+ file: zod_1.z.string().optional(),
83
+ scope: zod_1.z.enum(["file", "workspace"]).default("file"),
84
+ language: zod_1.z.string().optional(),
85
+ }),
86
+ }, async (args) => {
87
+ const filePath = typeof args.file === "string" ? args.file : "";
88
+ const scope = args.scope === "workspace" ? "workspace" : "file";
77
89
  if (filePath) {
78
90
  await lifecycleManager.ensureLanguageForFile(filePath);
79
91
  const client = lifecycleManager.getClientForFile(filePath);
80
92
  if (client) {
81
93
  const waitPromise = client.waitForDiagnosticsPublish(filePath, 10000);
82
94
  await client.ensureDidOpen(filePath);
83
- client.notify('textDocument/didSave', { textDocument: { uri: (0, uri_1.pathToUri)(filePath) } });
95
+ client.notify("textDocument/didSave", {
96
+ textDocument: { uri: (0, uri_1.pathToUri)(filePath) },
97
+ });
84
98
  await waitPromise;
85
99
  }
86
100
  }
87
- if (scope === 'workspace') {
88
- const language = typeof args.language === 'string' ? args.language : undefined;
89
- const diagnostics = lifecycleManager.getWorkspaceDiagnostics(language).slice(0, 200);
90
- return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, 'workspace'), diagnostics);
101
+ if (scope === "workspace") {
102
+ const language = typeof args.language === "string" ? args.language : undefined;
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);
91
108
  }
92
109
  const diagnostics = lifecycleManager.getFileDiagnostics(filePath);
93
- return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, 'file'), diagnostics);
94
- });
95
- registrar.registerTool('lsp_signature_help', { description: 'Show signature help', inputSchema: positionSchema }, async (args) => {
96
- return await runFileRequest({ args, lifecycleManager, method: 'textDocument/signatureHelp', timeoutMs: 5000, format: stringifyResult, raw: (result) => result });
110
+ return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "file"), diagnostics);
97
111
  });
98
- registrar.registerTool('lsp_type_definition', { description: 'Find type definitions', inputSchema: positionSchema }, async (args) => {
112
+ registrar.registerTool("lsp_type_definition", { description: "Find type definitions", inputSchema: positionSchema }, async (args) => {
99
113
  return await runFileRequest({
100
114
  args,
101
115
  lifecycleManager,
102
- method: 'textDocument/typeDefinition',
116
+ method: "textDocument/typeDefinition",
103
117
  timeoutMs: 5000,
104
118
  format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
105
- raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
119
+ raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result)),
106
120
  });
107
121
  });
108
- registrar.registerTool('lsp_implementation', { description: 'Find implementations', inputSchema: positionSchema }, async (args) => {
122
+ registrar.registerTool("lsp_implementation", { description: "Find implementations", inputSchema: positionSchema }, async (args) => {
109
123
  return await runFileRequest({
110
124
  args,
111
125
  lifecycleManager,
112
- method: 'textDocument/implementation',
126
+ method: "textDocument/implementation",
113
127
  timeoutMs: 5000,
114
128
  format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
115
- raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
129
+ raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result)),
116
130
  });
117
131
  });
118
- registrar.registerTool('lsp_health', { description: 'Show LSP server health', inputSchema: zod_1.z.object({}) }, async () => {
132
+ registrar.registerTool("lsp_health", { description: "Show LSP server health", inputSchema: zod_1.z.object({}) }, async () => {
119
133
  const health = lifecycleManager.getHealth();
120
134
  return (0, shared_1.success)((0, formatters_1.formatHealth)(health), health);
121
135
  });
122
136
  }
123
137
  async function handleInitTool(args, options) {
124
- const root = typeof args.root === 'string' ? args.root : '';
138
+ const root = typeof args.root === "string" ? args.root : "";
125
139
  if (root.length === 0) {
126
- return (0, shared_1.failure)('Project root is required. Provide lsp_init({ root: \'/absolute/path\' }).');
140
+ return (0, shared_1.failure)("Project root is required. Provide lsp_init({ root: '/absolute/path' }).");
127
141
  }
128
142
  if (!node_path_1.default.isAbsolute(root)) {
129
143
  return (0, shared_1.failure)(`Project root must be an absolute path: ${root}`);
@@ -138,23 +152,25 @@ async function handleInitTool(args, options) {
138
152
  return (0, shared_1.failure)(`Project root does not exist: ${root}`);
139
153
  }
140
154
  const languages = Array.isArray(args.languages)
141
- ? args.languages.filter((item) => typeof item === 'string')
155
+ ? args.languages.filter((item) => typeof item === "string")
142
156
  : undefined;
143
157
  try {
144
158
  const initialized = await options.initializeManager(root, languages);
145
159
  const languageNames = initialized.health.map((entry) => formatLanguageName(entry.language));
146
- const started = initialized.health.filter((entry) => entry.status === 'ready').length;
147
- const errors = initialized.health.filter((entry) => entry.status === 'error').length;
148
- const lazyNote = languageNames.length === 0 ? ' Language servers will start on first file access.' : '';
160
+ const started = initialized.health.filter((entry) => entry.status === "ready").length;
161
+ const errors = initialized.health.filter((entry) => entry.status === "error").length;
162
+ const lazyNote = languageNames.length === 0
163
+ ? " Language servers will start on first file access."
164
+ : "";
149
165
  const text = `Initialized LSP for ${initialized.root}. Detected languages: ${formatLanguageList(languageNames)}. LSP servers: ${started} started, ${errors} errors.${lazyNote}`;
150
166
  return {
151
- content: [{ type: 'text', text }],
167
+ content: [{ type: "text", text }],
152
168
  text,
153
169
  raw: {
154
170
  root: initialized.root,
155
171
  languages: languageNames,
156
- health: initialized.health
157
- }
172
+ health: initialized.health,
173
+ },
158
174
  };
159
175
  }
160
176
  catch (error) {
@@ -164,10 +180,10 @@ async function handleInitTool(args, options) {
164
180
  const positionSchema = zod_1.z.object({
165
181
  file: zod_1.z.string(),
166
182
  line: zod_1.z.number().int(),
167
- character: zod_1.z.number().int()
183
+ character: zod_1.z.number().int(),
168
184
  });
169
185
  async function runFileRequest(options) {
170
- const filePath = typeof options.args.file === 'string' ? options.args.file : '';
186
+ const filePath = typeof options.args.file === "string" ? options.args.file : "";
171
187
  await options.lifecycleManager.ensureLanguageForFile(filePath);
172
188
  const client = options.lifecycleManager.getClientForFile(filePath);
173
189
  if (!client) {
@@ -175,15 +191,15 @@ async function runFileRequest(options) {
175
191
  }
176
192
  const position = {
177
193
  line: Number(options.args.line ?? 0),
178
- character: Number(options.args.character ?? 0)
194
+ character: Number(options.args.character ?? 0),
179
195
  };
180
196
  const uri = (0, uri_1.pathToUri)(filePath);
181
197
  try {
182
198
  await client.ensureDidOpen(filePath);
183
- const result = await client.request(options.method, options.params?.(uri, position) ?? {
199
+ const result = (await client.request(options.method, options.params?.(uri, position) ?? {
184
200
  textDocument: { uri },
185
- position
186
- }, options.timeoutMs);
201
+ position,
202
+ }, options.timeoutMs));
187
203
  return (0, shared_1.success)(options.format(result), options.raw(result));
188
204
  }
189
205
  catch (error) {
@@ -196,24 +212,12 @@ function asLocationArray(result) {
196
212
  }
197
213
  return Array.isArray(result) ? result : [result];
198
214
  }
199
- function asCompletionItems(result) {
200
- if (!result) {
201
- return null;
202
- }
203
- return Array.isArray(result) ? result : result.items;
204
- }
205
- function stringifyResult(result) {
206
- if (!result) {
207
- return 'No result';
208
- }
209
- return JSON.stringify(result, null, 2);
210
- }
211
215
  function formatLanguageList(languages) {
212
- return languages.length === 0 ? 'None' : languages.join(', ');
216
+ return languages.length === 0 ? "None" : languages.join(", ");
213
217
  }
214
218
  function formatLanguageName(language) {
215
219
  if (language.length === 0) {
216
220
  return language;
217
221
  }
218
- return `${language[0]?.toUpperCase() ?? ''}${language.slice(1)}`;
222
+ return `${language[0]?.toUpperCase() ?? ""}${language.slice(1)}`;
219
223
  }
@@ -13,25 +13,25 @@ exports.normalizeSymbols = normalizeSymbols;
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const uri_1 = require("../../utils/uri");
15
15
  function success(text, raw) {
16
- return { content: [{ type: 'text', text }], raw };
16
+ return { content: [{ type: "text", text }], raw };
17
17
  }
18
18
  function failure(text, raw = null) {
19
- return { content: [{ type: 'text', text }], error: true, raw };
19
+ return { content: [{ type: "text", text }], error: true, raw };
20
20
  }
21
21
  function noServerResult(filePath) {
22
- return failure(`No language server available for ${node_path_1.default.extname(filePath) || 'unknown'} files. Run lsp_health for details.`);
22
+ return failure(`No language server available for ${node_path_1.default.extname(filePath) || "unknown"} files. Run lsp_health for details.`);
23
23
  }
24
24
  function noProjectRootResult() {
25
25
  const text = "No project root set. Call lsp_init({ root: '/path/to/project' }) first.";
26
- return { content: [{ type: 'text', text }], text, error: true, raw: null };
26
+ return { content: [{ type: "text", text }], text, error: true, raw: null };
27
27
  }
28
28
  function mapToolError(error, timeoutSeconds) {
29
29
  const message = error instanceof Error ? error.message : String(error);
30
- if (message.includes('timed out')) {
30
+ if (message.includes("timed out")) {
31
31
  return failure(`Operation timed out after ${timeoutSeconds}s — try a more specific query or check the LSP server health`);
32
32
  }
33
- if (message.includes('LSP server exited')) {
34
- return failure('Der Language Server ist neu gestartet, bitte versuche es erneut.');
33
+ if (message.includes("LSP server exited")) {
34
+ return failure("Der Language Server ist neu gestartet, bitte versuche es erneut.");
35
35
  }
36
36
  return failure(message, error);
37
37
  }
@@ -39,7 +39,10 @@ function normalizeLocations(locations) {
39
39
  if (!locations || locations.length === 0) {
40
40
  return null;
41
41
  }
42
- return locations.map((location) => ({ path: (0, uri_1.uriToPath)(location.uri), range: location.range }));
42
+ return locations.map((location) => ({
43
+ path: (0, uri_1.uriToPath)(location.uri),
44
+ range: location.range,
45
+ }));
43
46
  }
44
47
  function normalizeSymbols(symbols) {
45
48
  if (!symbols || symbols.length === 0) {
@@ -47,16 +50,21 @@ function normalizeSymbols(symbols) {
47
50
  }
48
51
  const normalized = [];
49
52
  for (const symbol of symbols) {
50
- if ('location' in symbol) {
53
+ if ("location" in symbol) {
51
54
  normalized.push({
52
55
  name: symbol.name,
53
56
  kind: symbol.kind,
54
57
  path: (0, uri_1.uriToPath)(symbol.location.uri),
55
- range: symbol.location.range
58
+ range: symbol.location.range,
56
59
  });
57
60
  continue;
58
61
  }
59
- normalized.push({ name: symbol.name, kind: symbol.kind, range: symbol.range, selectionRange: symbol.selectionRange });
62
+ normalized.push({
63
+ name: symbol.name,
64
+ kind: symbol.kind,
65
+ range: symbol.range,
66
+ selectionRange: symbol.selectionRange,
67
+ });
60
68
  }
61
69
  return normalized;
62
70
  }
@@ -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.1",
3
+ "version": "1.0.8",
4
4
  "description": "Universal LSP MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",