@theupsider/lsp-mcp 1.0.0 → 1.0.7

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,163 @@ 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 });
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
+ });
22
32
  });
23
- registrar.registerTool('lsp_definition', { description: 'Find definitions', inputSchema: positionSchema }, async (args) => {
33
+ registrar.registerTool("lsp_definition", { description: "Find definitions", inputSchema: positionSchema }, async (args) => {
24
34
  return await runFileRequest({
25
35
  args,
26
36
  lifecycleManager,
27
- method: 'textDocument/definition',
37
+ method: "textDocument/definition",
28
38
  timeoutMs: 5000,
29
39
  format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
30
- raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
40
+ raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result)),
31
41
  });
32
42
  });
33
- registrar.registerTool('lsp_references', { description: 'Find references', inputSchema: positionSchema.extend({ includeDeclaration: zod_1.z.boolean().optional() }) }, async (args) => {
43
+ registrar.registerTool("lsp_references", {
44
+ description: "Find references",
45
+ inputSchema: positionSchema.extend({
46
+ includeDeclaration: zod_1.z.boolean().optional(),
47
+ }),
48
+ }, async (args) => {
34
49
  const includeDeclaration = args.includeDeclaration === true;
35
50
  return await runFileRequest({
36
51
  args,
37
52
  lifecycleManager,
38
- method: 'textDocument/references',
53
+ method: "textDocument/references",
39
54
  timeoutMs: 15000,
40
- params: (uri, position) => ({ textDocument: { uri }, position, context: { includeDeclaration } }),
55
+ params: (uri, position) => ({
56
+ textDocument: { uri },
57
+ position,
58
+ context: { includeDeclaration },
59
+ }),
41
60
  format: formatters_1.formatReferences,
42
- raw: shared_1.normalizeLocations
61
+ raw: shared_1.normalizeLocations,
43
62
  });
44
63
  });
45
- registrar.registerTool('lsp_document_symbols', { description: 'List document symbols', inputSchema: zod_1.z.object({ file: zod_1.z.string() }) }, async (args) => {
64
+ registrar.registerTool("lsp_document_symbols", {
65
+ description: "List document symbols",
66
+ inputSchema: zod_1.z.object({ file: zod_1.z.string() }),
67
+ }, async (args) => {
46
68
  return await runFileRequest({
47
69
  args,
48
70
  lifecycleManager,
49
- method: 'textDocument/documentSymbol',
71
+ method: "textDocument/documentSymbol",
50
72
  timeoutMs: 15000,
51
73
  format: formatters_1.formatSymbols,
52
- raw: shared_1.normalizeSymbols
74
+ raw: shared_1.normalizeSymbols,
53
75
  });
54
76
  });
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 : '';
77
+ registrar.registerTool("lsp_workspace_symbols", {
78
+ description: "Search workspace symbols",
79
+ inputSchema: zod_1.z.object({ query: zod_1.z.string().default("") }),
80
+ }, async (args) => {
81
+ const query = typeof args.query === "string" ? args.query : "";
57
82
  await lifecycleManager.ensureSeedFilesOpen();
58
83
  const results = await Promise.all(lifecycleManager.getReadyClients().map(async (client) => {
59
- return await client.request('workspace/symbol', { query }, 30000);
84
+ return (await client.request("workspace/symbol", { query }, 30000));
60
85
  }));
61
- const merged = results.flat().slice(0, query === '' ? 100 : 500);
86
+ const merged = results.flat().slice(0, query === "" ? 100 : 500);
62
87
  return (0, shared_1.success)((0, formatters_1.formatSymbols)(merged), (0, shared_1.normalizeSymbols)(merged));
63
88
  });
64
- registrar.registerTool('lsp_completion', { description: 'Get completions', inputSchema: positionSchema }, async (args) => {
89
+ registrar.registerTool("lsp_completion", { description: "Get completions", inputSchema: positionSchema }, async (args) => {
65
90
  return await runFileRequest({
66
91
  args,
67
92
  lifecycleManager,
68
- method: 'textDocument/completion',
93
+ method: "textDocument/completion",
69
94
  timeoutMs: 5000,
70
95
  format: (result) => (0, formatters_1.formatCompletion)(asCompletionItems(result)),
71
- raw: (result) => asCompletionItems(result)?.slice(0, 50) ?? null
96
+ raw: (result) => asCompletionItems(result)?.slice(0, 50) ?? null,
72
97
  });
73
98
  });
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';
99
+ registrar.registerTool("lsp_diagnostics", {
100
+ 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
+ inputSchema: zod_1.z.object({
102
+ file: zod_1.z.string().optional(),
103
+ scope: zod_1.z.enum(["file", "workspace"]).default("file"),
104
+ language: zod_1.z.string().optional(),
105
+ }),
106
+ }, async (args) => {
107
+ const filePath = typeof args.file === "string" ? args.file : "";
108
+ const scope = args.scope === "workspace" ? "workspace" : "file";
77
109
  if (filePath) {
78
110
  await lifecycleManager.ensureLanguageForFile(filePath);
79
111
  const client = lifecycleManager.getClientForFile(filePath);
80
112
  if (client) {
81
113
  const waitPromise = client.waitForDiagnosticsPublish(filePath, 10000);
82
114
  await client.ensureDidOpen(filePath);
83
- client.notify('textDocument/didSave', { textDocument: { uri: (0, uri_1.pathToUri)(filePath) } });
115
+ client.notify("textDocument/didSave", {
116
+ textDocument: { uri: (0, uri_1.pathToUri)(filePath) },
117
+ });
84
118
  await waitPromise;
85
119
  }
86
120
  }
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);
121
+ if (scope === "workspace") {
122
+ const language = typeof args.language === "string" ? args.language : undefined;
123
+ await lifecycleManager.analyzeWorkspace(language);
124
+ const diagnostics = lifecycleManager
125
+ .getWorkspaceDiagnostics(language)
126
+ .slice(0, 200);
127
+ return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "workspace"), diagnostics);
91
128
  }
92
129
  const diagnostics = lifecycleManager.getFileDiagnostics(filePath);
93
- return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, 'file'), diagnostics);
130
+ return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "file"), diagnostics);
94
131
  });
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 });
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
+ });
97
141
  });
98
- registrar.registerTool('lsp_type_definition', { description: 'Find type definitions', inputSchema: positionSchema }, async (args) => {
142
+ registrar.registerTool("lsp_type_definition", { description: "Find type definitions", inputSchema: positionSchema }, async (args) => {
99
143
  return await runFileRequest({
100
144
  args,
101
145
  lifecycleManager,
102
- method: 'textDocument/typeDefinition',
146
+ method: "textDocument/typeDefinition",
103
147
  timeoutMs: 5000,
104
148
  format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
105
- raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
149
+ raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result)),
106
150
  });
107
151
  });
108
- registrar.registerTool('lsp_implementation', { description: 'Find implementations', inputSchema: positionSchema }, async (args) => {
152
+ registrar.registerTool("lsp_implementation", { description: "Find implementations", inputSchema: positionSchema }, async (args) => {
109
153
  return await runFileRequest({
110
154
  args,
111
155
  lifecycleManager,
112
- method: 'textDocument/implementation',
156
+ method: "textDocument/implementation",
113
157
  timeoutMs: 5000,
114
158
  format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
115
- raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
159
+ raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result)),
116
160
  });
117
161
  });
118
- registrar.registerTool('lsp_health', { description: 'Show LSP server health', inputSchema: zod_1.z.object({}) }, async () => {
162
+ registrar.registerTool("lsp_health", { description: "Show LSP server health", inputSchema: zod_1.z.object({}) }, async () => {
119
163
  const health = lifecycleManager.getHealth();
120
164
  return (0, shared_1.success)((0, formatters_1.formatHealth)(health), health);
121
165
  });
122
166
  }
123
167
  async function handleInitTool(args, options) {
124
- const root = typeof args.root === 'string' ? args.root : '';
168
+ const root = typeof args.root === "string" ? args.root : "";
125
169
  if (root.length === 0) {
126
- return (0, shared_1.failure)('Project root is required. Provide lsp_init({ root: \'/absolute/path\' }).');
170
+ return (0, shared_1.failure)("Project root is required. Provide lsp_init({ root: '/absolute/path' }).");
127
171
  }
128
172
  if (!node_path_1.default.isAbsolute(root)) {
129
173
  return (0, shared_1.failure)(`Project root must be an absolute path: ${root}`);
@@ -138,23 +182,25 @@ async function handleInitTool(args, options) {
138
182
  return (0, shared_1.failure)(`Project root does not exist: ${root}`);
139
183
  }
140
184
  const languages = Array.isArray(args.languages)
141
- ? args.languages.filter((item) => typeof item === 'string')
185
+ ? args.languages.filter((item) => typeof item === "string")
142
186
  : undefined;
143
187
  try {
144
188
  const initialized = await options.initializeManager(root, languages);
145
189
  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.' : '';
190
+ const started = initialized.health.filter((entry) => entry.status === "ready").length;
191
+ const errors = initialized.health.filter((entry) => entry.status === "error").length;
192
+ const lazyNote = languageNames.length === 0
193
+ ? " Language servers will start on first file access."
194
+ : "";
149
195
  const text = `Initialized LSP for ${initialized.root}. Detected languages: ${formatLanguageList(languageNames)}. LSP servers: ${started} started, ${errors} errors.${lazyNote}`;
150
196
  return {
151
- content: [{ type: 'text', text }],
197
+ content: [{ type: "text", text }],
152
198
  text,
153
199
  raw: {
154
200
  root: initialized.root,
155
201
  languages: languageNames,
156
- health: initialized.health
157
- }
202
+ health: initialized.health,
203
+ },
158
204
  };
159
205
  }
160
206
  catch (error) {
@@ -164,10 +210,10 @@ async function handleInitTool(args, options) {
164
210
  const positionSchema = zod_1.z.object({
165
211
  file: zod_1.z.string(),
166
212
  line: zod_1.z.number().int(),
167
- character: zod_1.z.number().int()
213
+ character: zod_1.z.number().int(),
168
214
  });
169
215
  async function runFileRequest(options) {
170
- const filePath = typeof options.args.file === 'string' ? options.args.file : '';
216
+ const filePath = typeof options.args.file === "string" ? options.args.file : "";
171
217
  await options.lifecycleManager.ensureLanguageForFile(filePath);
172
218
  const client = options.lifecycleManager.getClientForFile(filePath);
173
219
  if (!client) {
@@ -175,15 +221,15 @@ async function runFileRequest(options) {
175
221
  }
176
222
  const position = {
177
223
  line: Number(options.args.line ?? 0),
178
- character: Number(options.args.character ?? 0)
224
+ character: Number(options.args.character ?? 0),
179
225
  };
180
226
  const uri = (0, uri_1.pathToUri)(filePath);
181
227
  try {
182
228
  await client.ensureDidOpen(filePath);
183
- const result = await client.request(options.method, options.params?.(uri, position) ?? {
229
+ const result = (await client.request(options.method, options.params?.(uri, position) ?? {
184
230
  textDocument: { uri },
185
- position
186
- }, options.timeoutMs);
231
+ position,
232
+ }, options.timeoutMs));
187
233
  return (0, shared_1.success)(options.format(result), options.raw(result));
188
234
  }
189
235
  catch (error) {
@@ -204,16 +250,16 @@ function asCompletionItems(result) {
204
250
  }
205
251
  function stringifyResult(result) {
206
252
  if (!result) {
207
- return 'No result';
253
+ return "No result";
208
254
  }
209
255
  return JSON.stringify(result, null, 2);
210
256
  }
211
257
  function formatLanguageList(languages) {
212
- return languages.length === 0 ? 'None' : languages.join(', ');
258
+ return languages.length === 0 ? "None" : languages.join(", ");
213
259
  }
214
260
  function formatLanguageName(language) {
215
261
  if (language.length === 0) {
216
262
  return language;
217
263
  }
218
- return `${language[0]?.toUpperCase() ?? ''}${language.slice(1)}`;
264
+ return `${language[0]?.toUpperCase() ?? ""}${language.slice(1)}`;
219
265
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theupsider/lsp-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.7",
4
4
  "description": "Universal LSP MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",