mcp-lsp-driver 0.0.1
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/LICENSE +21 -0
- package/README.md +468 -0
- package/dist/capabilities.d.ts +181 -0
- package/dist/capabilities.js +11 -0
- package/dist/formatting.d.ts +33 -0
- package/dist/formatting.js +67 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +13 -0
- package/dist/interfaces.d.ts +38 -0
- package/dist/interfaces.js +8 -0
- package/dist/resolver.d.ts +81 -0
- package/dist/resolver.js +171 -0
- package/dist/schemas.d.ts +41 -0
- package/dist/schemas.js +62 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.js +557 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.js +8 -0
- package/package.json +52 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Implementation for LSP Driver SDK
|
|
3
|
+
*
|
|
4
|
+
* The SDK automatically registers tools based on which capability providers
|
|
5
|
+
* are defined in the IdeCapabilities configuration.
|
|
6
|
+
*/
|
|
7
|
+
import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { formatDiagnosticsAsMarkdown, formatSymbolsAsMarkdown, generateEditId, makeToolResult, normalizeUri, } from './formatting.js';
|
|
10
|
+
import { SymbolResolutionError, SymbolResolver, } from './resolver.js';
|
|
11
|
+
import { ApplyEditSchema, CallHierarchySchema, FuzzyPositionSchema, GlobalFindSchema, GlobalReplaceSchema, } from './schemas.js';
|
|
12
|
+
/**
|
|
13
|
+
* Register LSP-based tools and resources on the provided MCP server.
|
|
14
|
+
*/
|
|
15
|
+
export function installMcpLspDriver({ server, capabilities, config, }) {
|
|
16
|
+
const resolver = new SymbolResolver(capabilities.fileAccess, config?.resolverConfig);
|
|
17
|
+
try {
|
|
18
|
+
registerTools(server, capabilities, resolver);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error,
|
|
24
|
+
reason: 'Error occured during registration of tools',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
registerResources(server, capabilities);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error,
|
|
34
|
+
reason: 'Error occured during registration of resources',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function registerTools(server, capabilities, resolver) {
|
|
42
|
+
if (capabilities.definition) {
|
|
43
|
+
registerGotoDefinitionTool(server, capabilities, resolver);
|
|
44
|
+
}
|
|
45
|
+
if (capabilities.references) {
|
|
46
|
+
registerFindReferencesTool(server, capabilities, resolver);
|
|
47
|
+
}
|
|
48
|
+
if (capabilities.hierarchy) {
|
|
49
|
+
registerCallHierarchyTool(server, capabilities, resolver);
|
|
50
|
+
}
|
|
51
|
+
if (capabilities.userInteraction) {
|
|
52
|
+
registerApplyEditTool(server, capabilities, resolver);
|
|
53
|
+
}
|
|
54
|
+
if (capabilities.globalFind) {
|
|
55
|
+
registerGlobalFindTool(server, capabilities);
|
|
56
|
+
registerGlobalReplaceTool(server, capabilities);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function registerResources(server, capabilities) {
|
|
60
|
+
if (capabilities.diagnostics) {
|
|
61
|
+
registerDiagnosticsResources(server, capabilities);
|
|
62
|
+
}
|
|
63
|
+
if (capabilities.outline) {
|
|
64
|
+
registerOutlineResource(server, capabilities);
|
|
65
|
+
}
|
|
66
|
+
if (capabilities.filesystem) {
|
|
67
|
+
registerFilesystemResource(server, capabilities);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Registers the goto_definition tool.
|
|
72
|
+
*/
|
|
73
|
+
function registerGotoDefinitionTool(server, capabilities, resolver) {
|
|
74
|
+
const definitionProvider = capabilities.definition;
|
|
75
|
+
if (!definitionProvider)
|
|
76
|
+
return;
|
|
77
|
+
server.registerTool('goto_definition', {
|
|
78
|
+
description: 'Navigate to the definition of a symbol.',
|
|
79
|
+
inputSchema: FuzzyPositionSchema,
|
|
80
|
+
outputSchema: {
|
|
81
|
+
snippets: z.array(z.object({
|
|
82
|
+
uri: z.string(),
|
|
83
|
+
startLine: z.number(),
|
|
84
|
+
endLine: z.number(),
|
|
85
|
+
content: z.string(),
|
|
86
|
+
})),
|
|
87
|
+
},
|
|
88
|
+
}, async (params) => {
|
|
89
|
+
try {
|
|
90
|
+
const uri = normalizeUri(params.uri);
|
|
91
|
+
const fuzzy = {
|
|
92
|
+
symbolName: params.symbol_name,
|
|
93
|
+
lineHint: params.line_hint,
|
|
94
|
+
orderHint: params.order_hint,
|
|
95
|
+
};
|
|
96
|
+
const exactPosition = await resolver.resolvePosition(uri, fuzzy);
|
|
97
|
+
const snippets = (await definitionProvider.provideDefinition(uri, exactPosition)).map((snippet) => ({
|
|
98
|
+
uri: snippet.uri,
|
|
99
|
+
startLine: snippet.range.start.line + 1,
|
|
100
|
+
endLine: snippet.range.end.line + 1,
|
|
101
|
+
content: snippet.content,
|
|
102
|
+
}));
|
|
103
|
+
return makeToolResult({ snippets });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof SymbolResolutionError
|
|
107
|
+
? error.message
|
|
108
|
+
: `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text: message }],
|
|
111
|
+
structuredContent: { error: message },
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Registers the find_references tool.
|
|
119
|
+
*/
|
|
120
|
+
function registerFindReferencesTool(server, capabilities, resolver) {
|
|
121
|
+
const referencesProvider = capabilities.references;
|
|
122
|
+
if (!referencesProvider)
|
|
123
|
+
return;
|
|
124
|
+
server.registerTool('find_references', {
|
|
125
|
+
description: 'Find all references to a symbol. Returns a list of locations where the symbol is used.',
|
|
126
|
+
inputSchema: FuzzyPositionSchema,
|
|
127
|
+
outputSchema: {
|
|
128
|
+
snippets: z.array(z.object({
|
|
129
|
+
uri: z.string(),
|
|
130
|
+
startLine: z.number(),
|
|
131
|
+
endLine: z.number(),
|
|
132
|
+
content: z.string(),
|
|
133
|
+
})),
|
|
134
|
+
},
|
|
135
|
+
}, async (params) => {
|
|
136
|
+
try {
|
|
137
|
+
const uri = normalizeUri(params.uri);
|
|
138
|
+
const fuzzy = {
|
|
139
|
+
symbolName: params.symbol_name,
|
|
140
|
+
lineHint: params.line_hint,
|
|
141
|
+
orderHint: params.order_hint,
|
|
142
|
+
};
|
|
143
|
+
const exactPosition = await resolver.resolvePosition(uri, fuzzy);
|
|
144
|
+
const snippets = (await referencesProvider.provideReferences(uri, exactPosition)).map((snippet) => ({
|
|
145
|
+
uri: snippet.uri,
|
|
146
|
+
startLine: snippet.range.start.line + 1,
|
|
147
|
+
endLine: snippet.range.end.line + 1,
|
|
148
|
+
content: snippet.content,
|
|
149
|
+
}));
|
|
150
|
+
return makeToolResult({ snippets });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const message = error instanceof SymbolResolutionError
|
|
154
|
+
? error.message
|
|
155
|
+
: `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: 'text', text: message }],
|
|
158
|
+
structuredContent: { error: message },
|
|
159
|
+
isError: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Registers the call_hierarchy tool.
|
|
166
|
+
*/
|
|
167
|
+
function registerCallHierarchyTool(server, capabilities, resolver) {
|
|
168
|
+
const hierarchyProvider = capabilities.hierarchy;
|
|
169
|
+
if (!hierarchyProvider)
|
|
170
|
+
return;
|
|
171
|
+
server.registerTool('call_hierarchy', {
|
|
172
|
+
description: 'Get call hierarchy for a function or method. Shows incoming or outgoing calls.',
|
|
173
|
+
inputSchema: CallHierarchySchema,
|
|
174
|
+
outputSchema: {
|
|
175
|
+
snippets: z.array(z.object({
|
|
176
|
+
uri: z.string(),
|
|
177
|
+
startLine: z.number(),
|
|
178
|
+
endLine: z.number(),
|
|
179
|
+
content: z.string(),
|
|
180
|
+
})),
|
|
181
|
+
},
|
|
182
|
+
}, async (params) => {
|
|
183
|
+
try {
|
|
184
|
+
const uri = normalizeUri(params.uri);
|
|
185
|
+
const fuzzy = {
|
|
186
|
+
symbolName: params.symbol_name,
|
|
187
|
+
lineHint: params.line_hint,
|
|
188
|
+
orderHint: params.order_hint,
|
|
189
|
+
};
|
|
190
|
+
const exactPosition = await resolver.resolvePosition(uri, fuzzy);
|
|
191
|
+
const snippets = (await hierarchyProvider.provideCallHierarchy(uri, exactPosition, params.direction)).map((snippet) => ({
|
|
192
|
+
uri: snippet.uri,
|
|
193
|
+
startLine: snippet.range.start.line + 1,
|
|
194
|
+
endLine: snippet.range.end.line + 1,
|
|
195
|
+
content: snippet.content,
|
|
196
|
+
}));
|
|
197
|
+
return makeToolResult({ snippets });
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const message = error instanceof SymbolResolutionError
|
|
201
|
+
? error.message
|
|
202
|
+
: `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: 'text', text: message }],
|
|
205
|
+
structuredContent: { error: message },
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Registers diagnostics resources.
|
|
213
|
+
* - lsp://diagnostics/{path} - diagnostics for a specific file
|
|
214
|
+
* - lsp://diagnostics/workspace - diagnostics for the entire workspace (if getWorkspaceDiagnostics is provided)
|
|
215
|
+
*/
|
|
216
|
+
function registerDiagnosticsResources(server, capabilities) {
|
|
217
|
+
const diagnosticsProvider = capabilities.diagnostics;
|
|
218
|
+
if (!diagnosticsProvider)
|
|
219
|
+
return;
|
|
220
|
+
// Register file diagnostics resource template
|
|
221
|
+
const fileDiagnosticsTemplate = new ResourceTemplate('lsp://diagnostics/{+path}', {
|
|
222
|
+
list: undefined, // Cannot enumerate all files with diagnostics
|
|
223
|
+
});
|
|
224
|
+
server.registerResource('file-diagnostics', fileDiagnosticsTemplate, {
|
|
225
|
+
description: 'Diagnostics (errors, warnings, hints) for a specific file. Use the file path after lsp://diagnostics/',
|
|
226
|
+
mimeType: 'text/markdown',
|
|
227
|
+
}, async (_uri, variables) => {
|
|
228
|
+
try {
|
|
229
|
+
const path = variables.path;
|
|
230
|
+
const normalizedPath = normalizeUri(path);
|
|
231
|
+
const diagnostics = await diagnosticsProvider.provideDiagnostics(normalizedPath);
|
|
232
|
+
const markdown = formatDiagnosticsAsMarkdown(diagnostics);
|
|
233
|
+
return {
|
|
234
|
+
contents: [
|
|
235
|
+
{
|
|
236
|
+
uri: `lsp://diagnostics/${path}`,
|
|
237
|
+
mimeType: 'text/markdown',
|
|
238
|
+
text: markdown,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
245
|
+
return {
|
|
246
|
+
contents: [
|
|
247
|
+
{
|
|
248
|
+
uri: `lsp://diagnostics/${variables.path}`,
|
|
249
|
+
mimeType: 'text/markdown',
|
|
250
|
+
text: message,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// Register workspace diagnostics resource if getWorkspaceDiagnostics is provided
|
|
257
|
+
if (diagnosticsProvider.getWorkspaceDiagnostics) {
|
|
258
|
+
const getWorkspaceDiagnostics = diagnosticsProvider.getWorkspaceDiagnostics.bind(diagnosticsProvider);
|
|
259
|
+
server.registerResource('workspace-diagnostics', 'lsp://diagnostics/workspace', {
|
|
260
|
+
description: 'All diagnostics (errors, warnings, hints) across the entire workspace',
|
|
261
|
+
mimeType: 'text/markdown',
|
|
262
|
+
}, async () => {
|
|
263
|
+
try {
|
|
264
|
+
const diagnostics = await getWorkspaceDiagnostics();
|
|
265
|
+
// Group diagnostics by file
|
|
266
|
+
const groupedByFile = new Map();
|
|
267
|
+
for (const d of diagnostics) {
|
|
268
|
+
const existing = groupedByFile.get(d.uri) ?? [];
|
|
269
|
+
existing.push(d);
|
|
270
|
+
groupedByFile.set(d.uri, existing);
|
|
271
|
+
}
|
|
272
|
+
if (groupedByFile.size === 0) {
|
|
273
|
+
return {
|
|
274
|
+
contents: [
|
|
275
|
+
{
|
|
276
|
+
uri: 'lsp://diagnostics/workspace',
|
|
277
|
+
mimeType: 'text/markdown',
|
|
278
|
+
text: 'No diagnostics found in workspace.',
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// Format grouped diagnostics
|
|
284
|
+
const sections = [];
|
|
285
|
+
for (const [uri, fileDiagnostics] of groupedByFile) {
|
|
286
|
+
sections.push(`## ${uri}\n${formatDiagnosticsAsMarkdown(fileDiagnostics)}`);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
contents: [
|
|
290
|
+
{
|
|
291
|
+
uri: 'lsp://diagnostics/workspace',
|
|
292
|
+
mimeType: 'text/markdown',
|
|
293
|
+
text: sections.join('\n\n'),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
300
|
+
return {
|
|
301
|
+
contents: [
|
|
302
|
+
{
|
|
303
|
+
uri: 'lsp://diagnostics/workspace',
|
|
304
|
+
mimeType: 'text/markdown',
|
|
305
|
+
text: message,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Set up subscription support if onDiagnosticsChanged is provided
|
|
313
|
+
if (capabilities.onDiagnosticsChanged) {
|
|
314
|
+
capabilities.onDiagnosticsChanged((uri) => {
|
|
315
|
+
// Notify MCP clients that the diagnostics resource has been updated
|
|
316
|
+
const normalizedUri = normalizeUri(uri);
|
|
317
|
+
server.server.sendResourceUpdated({
|
|
318
|
+
uri: `lsp://diagnostics/${normalizedUri}`,
|
|
319
|
+
});
|
|
320
|
+
// Also notify workspace diagnostics if it exists
|
|
321
|
+
if (diagnosticsProvider.getWorkspaceDiagnostics) {
|
|
322
|
+
server.server.sendResourceUpdated({
|
|
323
|
+
uri: 'lsp://diagnostics/workspace',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Registers the outline resource.
|
|
331
|
+
* - lsp://outline/{path} - document symbols (outline) for a specific file
|
|
332
|
+
*/
|
|
333
|
+
function registerOutlineResource(server, capabilities) {
|
|
334
|
+
const outlineProvider = capabilities.outline;
|
|
335
|
+
if (!outlineProvider)
|
|
336
|
+
return;
|
|
337
|
+
const outlineTemplate = new ResourceTemplate('lsp://outline/{+path}', {
|
|
338
|
+
list: undefined, // Cannot enumerate all files
|
|
339
|
+
});
|
|
340
|
+
server.registerResource('file-outline', outlineTemplate, {
|
|
341
|
+
description: 'Document outline (symbols like classes, functions, variables) for a specific file. Use the file path after lsp://outline/',
|
|
342
|
+
mimeType: 'text/markdown',
|
|
343
|
+
}, async (_uri, variables) => {
|
|
344
|
+
try {
|
|
345
|
+
const path = variables.path;
|
|
346
|
+
const normalizedPath = normalizeUri(path);
|
|
347
|
+
const symbols = await outlineProvider.provideDocumentSymbols(normalizedPath);
|
|
348
|
+
const markdown = formatSymbolsAsMarkdown(symbols);
|
|
349
|
+
return {
|
|
350
|
+
contents: [
|
|
351
|
+
{
|
|
352
|
+
uri: `lsp://outline/${path}`,
|
|
353
|
+
mimeType: 'text/markdown',
|
|
354
|
+
text: markdown,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
361
|
+
return {
|
|
362
|
+
contents: [
|
|
363
|
+
{
|
|
364
|
+
uri: `lsp://outline/${variables.path}`,
|
|
365
|
+
mimeType: 'text/markdown',
|
|
366
|
+
text: message,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Registers the apply_edit tool.
|
|
375
|
+
*/
|
|
376
|
+
function registerApplyEditTool(server, capabilities, resolver) {
|
|
377
|
+
const userInteraction = capabilities.userInteraction;
|
|
378
|
+
if (!userInteraction)
|
|
379
|
+
return;
|
|
380
|
+
server.registerTool('apply_edit', {
|
|
381
|
+
description: 'Apply a text edit to a file. The edit must be approved by the user before being applied.',
|
|
382
|
+
inputSchema: {
|
|
383
|
+
uri: ApplyEditSchema.shape.uri,
|
|
384
|
+
search_text: ApplyEditSchema.shape.search_text,
|
|
385
|
+
replace_text: ApplyEditSchema.shape.replace_text,
|
|
386
|
+
description: ApplyEditSchema.shape.description,
|
|
387
|
+
},
|
|
388
|
+
outputSchema: {
|
|
389
|
+
success: z.boolean(),
|
|
390
|
+
message: z.string(),
|
|
391
|
+
},
|
|
392
|
+
}, async (params) => {
|
|
393
|
+
try {
|
|
394
|
+
const uri = normalizeUri(params.uri);
|
|
395
|
+
// Validate that the search text exists and is unique
|
|
396
|
+
const range = await resolver.findExactText(uri, params.search_text);
|
|
397
|
+
// Create pending edit operation
|
|
398
|
+
const operation = {
|
|
399
|
+
id: generateEditId(),
|
|
400
|
+
uri,
|
|
401
|
+
edits: [
|
|
402
|
+
{
|
|
403
|
+
range,
|
|
404
|
+
newText: params.replace_text,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
description: params.description,
|
|
408
|
+
};
|
|
409
|
+
// Request user approval
|
|
410
|
+
const approved = await userInteraction.previewAndApplyEdits(operation);
|
|
411
|
+
const result = approved
|
|
412
|
+
? { success: true, message: 'Edit successfully applied and saved.' }
|
|
413
|
+
: {
|
|
414
|
+
success: false,
|
|
415
|
+
message: 'Edit rejected by user.',
|
|
416
|
+
};
|
|
417
|
+
return makeToolResult(result);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: 'text', text: message }],
|
|
423
|
+
structuredContent: {
|
|
424
|
+
success: false,
|
|
425
|
+
message,
|
|
426
|
+
},
|
|
427
|
+
isError: true,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Registers the filesystem resource.
|
|
434
|
+
* - lsp://files/{path} - file tree for a directory (git-ignored files excluded)
|
|
435
|
+
*/
|
|
436
|
+
function registerFilesystemResource(server, capabilities) {
|
|
437
|
+
const filesystemProvider = capabilities.filesystem;
|
|
438
|
+
if (!filesystemProvider)
|
|
439
|
+
return;
|
|
440
|
+
const filesystemTemplate = new ResourceTemplate('lsp://files/{+path}', {
|
|
441
|
+
list: undefined, // Cannot enumerate all directories
|
|
442
|
+
});
|
|
443
|
+
server.registerResource('filesystem', filesystemTemplate, {
|
|
444
|
+
description: 'File tree for a directory, excluding git-ignored files. Use the folder path after lsp://files/',
|
|
445
|
+
mimeType: 'text/markdown',
|
|
446
|
+
}, async (_uri, variables) => {
|
|
447
|
+
try {
|
|
448
|
+
const path = variables.path;
|
|
449
|
+
const normalizedPath = normalizeUri(path);
|
|
450
|
+
const files = await filesystemProvider.getFileTree(normalizedPath);
|
|
451
|
+
const markdown = files.length === 0
|
|
452
|
+
? 'No files found in directory.'
|
|
453
|
+
: files.map((f) => `- ${f}`).join('\n');
|
|
454
|
+
return {
|
|
455
|
+
contents: [
|
|
456
|
+
{
|
|
457
|
+
uri: `lsp://files/${path}`,
|
|
458
|
+
mimeType: 'text/markdown',
|
|
459
|
+
text: markdown,
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
466
|
+
return {
|
|
467
|
+
contents: [
|
|
468
|
+
{
|
|
469
|
+
uri: `lsp://files/${variables.path}`,
|
|
470
|
+
mimeType: 'text/markdown',
|
|
471
|
+
text: message,
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Registers the global_find tool.
|
|
480
|
+
*/
|
|
481
|
+
function registerGlobalFindTool(server, capabilities) {
|
|
482
|
+
const globalFindProvider = capabilities.globalFind;
|
|
483
|
+
if (!globalFindProvider)
|
|
484
|
+
return;
|
|
485
|
+
server.registerTool('global_find', {
|
|
486
|
+
description: 'Search for text across the entire workspace.',
|
|
487
|
+
inputSchema: GlobalFindSchema,
|
|
488
|
+
outputSchema: {
|
|
489
|
+
matches: z.array(z.object({
|
|
490
|
+
uri: z.string(),
|
|
491
|
+
line: z.number(),
|
|
492
|
+
column: z.number(),
|
|
493
|
+
matchText: z.string(),
|
|
494
|
+
context: z.string(),
|
|
495
|
+
})),
|
|
496
|
+
count: z.number(),
|
|
497
|
+
},
|
|
498
|
+
}, async (params) => {
|
|
499
|
+
try {
|
|
500
|
+
const caseSensitive = params.case_sensitive ?? false;
|
|
501
|
+
const exactMatch = params.exact_match ?? false;
|
|
502
|
+
const regexMode = params.regex_mode ?? false;
|
|
503
|
+
const matches = await globalFindProvider.globalFind(params.query, {
|
|
504
|
+
caseSensitive,
|
|
505
|
+
exactMatch,
|
|
506
|
+
regexMode,
|
|
507
|
+
});
|
|
508
|
+
return makeToolResult({ count: matches.length, matches });
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
512
|
+
return {
|
|
513
|
+
content: [{ type: 'text', text: message }],
|
|
514
|
+
structuredContent: { error: message },
|
|
515
|
+
isError: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Registers the global_replace tool.
|
|
522
|
+
*/
|
|
523
|
+
function registerGlobalReplaceTool(server, capabilities) {
|
|
524
|
+
const globalFindProvider = capabilities.globalFind;
|
|
525
|
+
if (!globalFindProvider)
|
|
526
|
+
return;
|
|
527
|
+
server.registerTool('global_replace', {
|
|
528
|
+
description: 'Replace all occurrences of text across the entire workspace.',
|
|
529
|
+
inputSchema: GlobalReplaceSchema,
|
|
530
|
+
outputSchema: {
|
|
531
|
+
success: z.boolean(),
|
|
532
|
+
count: z.number(),
|
|
533
|
+
message: z.string().optional(),
|
|
534
|
+
},
|
|
535
|
+
}, async (params) => {
|
|
536
|
+
try {
|
|
537
|
+
const caseSensitive = params.case_sensitive ?? false;
|
|
538
|
+
const exactMatch = params.exact_match ?? false;
|
|
539
|
+
const regexMode = params.regex_mode ?? false;
|
|
540
|
+
const count = await globalFindProvider.globalReplace(params.query, params.replace_with, { caseSensitive, exactMatch, regexMode });
|
|
541
|
+
return makeToolResult({ success: true, count });
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
const message = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
545
|
+
return {
|
|
546
|
+
content: [{ type: 'text', text: message }],
|
|
547
|
+
structuredContent: {
|
|
548
|
+
success: false,
|
|
549
|
+
replacementCount: 0,
|
|
550
|
+
message,
|
|
551
|
+
},
|
|
552
|
+
isError: true,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
//# sourceMappingURL=server.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Data Models for MCP LSP Driver SDK
|
|
3
|
+
*
|
|
4
|
+
* These types define how the LLM communicates intent versus
|
|
5
|
+
* how the IDE executes commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* A Unified Resource Identifier.
|
|
9
|
+
* Must be a file system path or a standard file:// scheme.
|
|
10
|
+
*/
|
|
11
|
+
export type UnifiedUri = string;
|
|
12
|
+
/**
|
|
13
|
+
* 0-based exact coordinate system (Used internally by IDE).
|
|
14
|
+
*/
|
|
15
|
+
export interface ExactPosition {
|
|
16
|
+
/** 0-based line number */
|
|
17
|
+
line: number;
|
|
18
|
+
/** 0-based column (character offset) */
|
|
19
|
+
character: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The fuzzy location provided by the LLM.
|
|
23
|
+
* Designed to be robust against minor formatting changes or token counting errors.
|
|
24
|
+
*/
|
|
25
|
+
export interface FuzzyPosition {
|
|
26
|
+
/**
|
|
27
|
+
* The text of the symbol to find (e.g., function name, variable name).
|
|
28
|
+
*/
|
|
29
|
+
symbolName: string;
|
|
30
|
+
/**
|
|
31
|
+
* An approximate line number where the symbol is expected.
|
|
32
|
+
* 1-based (Human friendly) for LLM input, converted to 0-based internally.
|
|
33
|
+
*/
|
|
34
|
+
lineHint: number;
|
|
35
|
+
/**
|
|
36
|
+
* If the symbol appears multiple times on the line, which occurrence to target?
|
|
37
|
+
* 0-based index. Defaults to 0 (the first occurrence).
|
|
38
|
+
* Example: "add(a, a)" -> looking for second 'a' -> orderHint: 1.
|
|
39
|
+
*/
|
|
40
|
+
orderHint?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Represents a resolved span of text on disk.
|
|
44
|
+
*/
|
|
45
|
+
export interface DiskRange {
|
|
46
|
+
start: ExactPosition;
|
|
47
|
+
end: ExactPosition;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Represents a snippet of code read from disk.
|
|
51
|
+
*/
|
|
52
|
+
export interface CodeSnippet {
|
|
53
|
+
/** The URI of the file containing the snippet */
|
|
54
|
+
uri: UnifiedUri;
|
|
55
|
+
/** The range of the snippet in the file */
|
|
56
|
+
range: DiskRange;
|
|
57
|
+
/** The actual text read from disk */
|
|
58
|
+
content: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Represents a proposed change to a file.
|
|
62
|
+
*/
|
|
63
|
+
export interface TextEdit {
|
|
64
|
+
/** The range to replace */
|
|
65
|
+
range: DiskRange;
|
|
66
|
+
/** The new text to insert */
|
|
67
|
+
newText: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Represents a pending edit operation that awaits user approval.
|
|
71
|
+
*/
|
|
72
|
+
export interface PendingEditOperation {
|
|
73
|
+
/** Unique identifier for this operation */
|
|
74
|
+
id: string;
|
|
75
|
+
/** The URI of the file to edit */
|
|
76
|
+
uri: UnifiedUri;
|
|
77
|
+
/** The list of edits to apply */
|
|
78
|
+
edits: TextEdit[];
|
|
79
|
+
/** Optional description of the edit (e.g., "Refactor logic to handle null cases") */
|
|
80
|
+
description?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The reason why an edit operation failed.
|
|
84
|
+
*/
|
|
85
|
+
export type EditFailureReason = 'UserRejected' | 'IOError' | 'ValidationFailed';
|
|
86
|
+
/**
|
|
87
|
+
* The result of an edit operation.
|
|
88
|
+
*/
|
|
89
|
+
export type EditResult = {
|
|
90
|
+
success: boolean;
|
|
91
|
+
message: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Severity level for diagnostics.
|
|
95
|
+
*/
|
|
96
|
+
export type DiagnosticSeverity = 'error' | 'warning' | 'information' | 'hint';
|
|
97
|
+
/**
|
|
98
|
+
* Represents a diagnostic (error, warning, etc.) from the IDE.
|
|
99
|
+
*/
|
|
100
|
+
export interface Diagnostic {
|
|
101
|
+
/** The URI of the file containing the diagnostic */
|
|
102
|
+
uri: UnifiedUri;
|
|
103
|
+
/** The range of the diagnostic */
|
|
104
|
+
range: DiskRange;
|
|
105
|
+
/** The severity of the diagnostic */
|
|
106
|
+
severity: DiagnosticSeverity;
|
|
107
|
+
/** The diagnostic message */
|
|
108
|
+
message: string;
|
|
109
|
+
/** Optional source of the diagnostic (e.g., "typescript", "eslint") */
|
|
110
|
+
source?: string;
|
|
111
|
+
/** Optional diagnostic code */
|
|
112
|
+
code?: string | number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Symbol kind for outline items.
|
|
116
|
+
*/
|
|
117
|
+
export type SymbolKind = 'file' | 'module' | 'namespace' | 'package' | 'class' | 'method' | 'property' | 'field' | 'constructor' | 'enum' | 'interface' | 'function' | 'variable' | 'constant' | 'string' | 'number' | 'boolean' | 'array' | 'object' | 'key' | 'null' | 'enumMember' | 'struct' | 'event' | 'operator' | 'typeParameter';
|
|
118
|
+
/**
|
|
119
|
+
* Represents a symbol in the document outline (e.g., class, function, variable).
|
|
120
|
+
*/
|
|
121
|
+
export interface DocumentSymbol {
|
|
122
|
+
/** The name of the symbol */
|
|
123
|
+
name: string;
|
|
124
|
+
/** More detail for this symbol (e.g., signature) */
|
|
125
|
+
detail?: string;
|
|
126
|
+
/** The kind of this symbol */
|
|
127
|
+
kind: SymbolKind;
|
|
128
|
+
/** The range of the entire symbol (including body) */
|
|
129
|
+
range: DiskRange;
|
|
130
|
+
/** The range of the symbol's name */
|
|
131
|
+
selectionRange: DiskRange;
|
|
132
|
+
/** Children of this symbol (e.g., methods in a class) */
|
|
133
|
+
children?: DocumentSymbol[];
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=types.d.ts.map
|