@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.
- package/dist/lsp/lifecycle-manager.js +22 -7
- package/dist/lsp/lsp-client.js +57 -4
- package/dist/lsp/server-supervisor.js +17 -18
- package/dist/mcp/__tests__/read-tools.test.js +354 -146
- package/dist/mcp/__tests__/write-tools.test.js +218 -104
- package/dist/mcp/formatters.js +109 -91
- package/dist/mcp/server.js +30 -14
- package/dist/mcp/tools/read-tools.js +104 -58
- package/dist/mcp/tools/shared.js +19 -11
- package/package.json +1 -1
|
@@ -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(
|
|
15
|
-
description:
|
|
16
|
-
inputSchema: zod_1.z.object({
|
|
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(
|
|
21
|
-
return await runFileRequest({
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
53
|
+
method: "textDocument/references",
|
|
39
54
|
timeoutMs: 15000,
|
|
40
|
-
params: (uri, position) => ({
|
|
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(
|
|
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:
|
|
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(
|
|
56
|
-
|
|
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(
|
|
84
|
+
return (await client.request("workspace/symbol", { query }, 30000));
|
|
60
85
|
}));
|
|
61
|
-
const merged = results.flat().slice(0, query ===
|
|
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(
|
|
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:
|
|
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(
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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 ===
|
|
88
|
-
const language = typeof args.language ===
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
130
|
+
return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, "file"), diagnostics);
|
|
94
131
|
});
|
|
95
|
-
registrar.registerTool(
|
|
96
|
-
return await runFileRequest({
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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 ===
|
|
168
|
+
const root = typeof args.root === "string" ? args.root : "";
|
|
125
169
|
if (root.length === 0) {
|
|
126
|
-
return (0, shared_1.failure)(
|
|
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 ===
|
|
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 ===
|
|
147
|
-
const errors = initialized.health.filter((entry) => entry.status ===
|
|
148
|
-
const lazyNote = languageNames.length === 0
|
|
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:
|
|
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 ===
|
|
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
|
|
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 ?
|
|
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() ??
|
|
264
|
+
return `${language[0]?.toUpperCase() ?? ""}${language.slice(1)}`;
|
|
219
265
|
}
|
package/dist/mcp/tools/shared.js
CHANGED
|
@@ -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:
|
|
16
|
+
return { content: [{ type: "text", text }], raw };
|
|
17
17
|
}
|
|
18
18
|
function failure(text, raw = null) {
|
|
19
|
-
return { content: [{ type:
|
|
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) ||
|
|
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:
|
|
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(
|
|
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(
|
|
34
|
-
return failure(
|
|
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) => ({
|
|
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 (
|
|
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({
|
|
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
|
}
|