@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.
- package/README.md +2 -6
- 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 +346 -151
- package/dist/mcp/__tests__/server.test.js +6 -6
- package/dist/mcp/__tests__/write-tools.test.js +190 -126
- package/dist/mcp/formatters.js +109 -91
- package/dist/mcp/server.js +30 -14
- package/dist/mcp/tools/read-tools.js +82 -78
- package/dist/mcp/tools/shared.js +19 -11
- package/dist/mcp/tools/write-tools.js +0 -13
- package/package.json +1 -1
|
@@ -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(
|
|
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({ 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:
|
|
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(
|
|
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:
|
|
43
|
+
method: "textDocument/references",
|
|
39
44
|
timeoutMs: 15000,
|
|
40
|
-
params: (uri, position) => ({
|
|
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(
|
|
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:
|
|
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(
|
|
56
|
-
|
|
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(
|
|
74
|
+
return (await client.request("workspace/symbol", { query }, 30000));
|
|
60
75
|
}));
|
|
61
|
-
const merged = results.flat().slice(0, query ===
|
|
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(
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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(
|
|
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 ===
|
|
88
|
-
const language = typeof args.language ===
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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 ===
|
|
138
|
+
const root = typeof args.root === "string" ? args.root : "";
|
|
125
139
|
if (root.length === 0) {
|
|
126
|
-
return (0, shared_1.failure)(
|
|
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 ===
|
|
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 ===
|
|
147
|
-
const errors = initialized.health.filter((entry) => entry.status ===
|
|
148
|
-
const lazyNote = languageNames.length === 0
|
|
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:
|
|
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 ===
|
|
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 ?
|
|
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() ??
|
|
222
|
+
return `${language[0]?.toUpperCase() ?? ""}${language.slice(1)}`;
|
|
219
223
|
}
|
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
|
}
|
|
@@ -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 });
|