@theupsider/lsp-mcp 0.1.0
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 +173 -0
- package/README.md +188 -0
- package/dist/__tests__/index.test.js +45 -0
- package/dist/detection/__tests__/language-detector.test.js +121 -0
- package/dist/detection/__tests__/lsp-mapping.test.js +78 -0
- package/dist/detection/language-detector.js +107 -0
- package/dist/detection/lsp-mapping.js +86 -0
- package/dist/index.js +60 -0
- package/dist/lsp/__tests__/installer.test.js +113 -0
- package/dist/lsp/__tests__/lifecycle-manager.test.js +288 -0
- package/dist/lsp/__tests__/lsp-client.test.js +238 -0
- package/dist/lsp/installer.js +65 -0
- package/dist/lsp/lifecycle-manager.js +272 -0
- package/dist/lsp/lsp-client.js +226 -0
- package/dist/mcp/__tests__/formatters.test.js +153 -0
- package/dist/mcp/__tests__/read-tools.test.js +281 -0
- package/dist/mcp/__tests__/server.test.js +140 -0
- package/dist/mcp/__tests__/write-tools.test.js +257 -0
- package/dist/mcp/formatters.js +202 -0
- package/dist/mcp/server.js +117 -0
- package/dist/mcp/tools/read-tools.js +208 -0
- package/dist/mcp/tools/shared.js +106 -0
- package/dist/mcp/tools/write-tools.js +252 -0
- package/dist/utils/__tests__/uri.test.js +21 -0
- package/dist/utils/uri.js +43 -0
- package/package.json +32 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpServer = void 0;
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const zod_compat_js_1 = require("@modelcontextprotocol/sdk/server/zod-compat.js");
|
|
6
|
+
const zod_json_schema_compat_js_1 = require("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js");
|
|
7
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
9
|
+
const lifecycle_manager_1 = require("../lsp/lifecycle-manager");
|
|
10
|
+
const shared_1 = require("./tools/shared");
|
|
11
|
+
const read_tools_1 = require("./tools/read-tools");
|
|
12
|
+
const write_tools_1 = require("./tools/write-tools");
|
|
13
|
+
class McpServer {
|
|
14
|
+
currentManager = null;
|
|
15
|
+
currentRoot = null;
|
|
16
|
+
initialized = false;
|
|
17
|
+
logLevel;
|
|
18
|
+
createLifecycleManager;
|
|
19
|
+
server;
|
|
20
|
+
tools = new Map();
|
|
21
|
+
constructor(logLevel = 'info', createLifecycleManager = (root, level) => new lifecycle_manager_1.LifecycleManager(root, level)) {
|
|
22
|
+
this.logLevel = logLevel;
|
|
23
|
+
this.createLifecycleManager = createLifecycleManager;
|
|
24
|
+
this.server = new mcp_js_1.McpServer({ name: '@theupsider/lsp-mcp', version: '0.1.0' });
|
|
25
|
+
}
|
|
26
|
+
async start() {
|
|
27
|
+
const lifecycleProxy = this.createLifecycleProxy();
|
|
28
|
+
const registrar = {
|
|
29
|
+
registerTool: (name, config, handler) => {
|
|
30
|
+
this.tools.set(name, { name, description: config.description, inputSchema: config.inputSchema, handler });
|
|
31
|
+
},
|
|
32
|
+
fromJsonSchema: (schema) => schema
|
|
33
|
+
};
|
|
34
|
+
(0, read_tools_1.registerReadTools)(registrar, lifecycleProxy, { initializeManager: async (root, languages) => await this.initializeManager(root, languages) });
|
|
35
|
+
(0, write_tools_1.registerWriteTools)(registrar, lifecycleProxy);
|
|
36
|
+
this.configureToolHandlers();
|
|
37
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
38
|
+
await this.server.connect(transport);
|
|
39
|
+
}
|
|
40
|
+
async initializeManager(root, languages) {
|
|
41
|
+
const nextManager = this.createLifecycleManager(root, this.logLevel);
|
|
42
|
+
const previousManager = this.currentManager;
|
|
43
|
+
if (previousManager) {
|
|
44
|
+
await previousManager.shutdown();
|
|
45
|
+
this.currentManager = null;
|
|
46
|
+
this.currentRoot = null;
|
|
47
|
+
}
|
|
48
|
+
await nextManager.start(languages);
|
|
49
|
+
this.setManager(nextManager);
|
|
50
|
+
this.currentRoot = root;
|
|
51
|
+
return { root, health: nextManager.getHealth() };
|
|
52
|
+
}
|
|
53
|
+
setManager(lifecycleManager) {
|
|
54
|
+
this.currentManager = lifecycleManager;
|
|
55
|
+
this.initialized = true;
|
|
56
|
+
}
|
|
57
|
+
async shutdown() {
|
|
58
|
+
const activeManager = this.currentManager;
|
|
59
|
+
this.currentManager = null;
|
|
60
|
+
this.currentRoot = null;
|
|
61
|
+
this.initialized = false;
|
|
62
|
+
if (activeManager) {
|
|
63
|
+
await activeManager.shutdown();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
configureToolHandlers() {
|
|
67
|
+
this.server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: this.listTools() }));
|
|
68
|
+
this.server.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
69
|
+
const toolName = request.params.name;
|
|
70
|
+
if (toolName === 'lsp_init' && this.initialized) {
|
|
71
|
+
return (0, shared_1.failure)(`Already initialized at ${this.currentRoot ?? 'unknown'}. Call lsp_init again with a new root to switch projects.`);
|
|
72
|
+
}
|
|
73
|
+
if (toolName !== 'lsp_init' && !this.initialized) {
|
|
74
|
+
return (0, shared_1.noProjectRootResult)();
|
|
75
|
+
}
|
|
76
|
+
const tool = this.tools.get(toolName);
|
|
77
|
+
if (!tool) {
|
|
78
|
+
return (0, shared_1.failure)(`Tool not found: ${toolName}`);
|
|
79
|
+
}
|
|
80
|
+
return await tool.handler(isRecord(request.params.arguments) ? request.params.arguments : {});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
listTools() {
|
|
84
|
+
return [...this.tools.values()].map((tool) => ({
|
|
85
|
+
name: tool.name,
|
|
86
|
+
description: tool.description,
|
|
87
|
+
inputSchema: this.toInputSchema(tool.inputSchema)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
toInputSchema(schema) {
|
|
91
|
+
const normalized = (0, zod_compat_js_1.normalizeObjectSchema)(schema);
|
|
92
|
+
const result = normalized
|
|
93
|
+
? (0, zod_json_schema_compat_js_1.toJsonSchemaCompat)(normalized, { strictUnions: true, pipeStrategy: 'input' })
|
|
94
|
+
: {};
|
|
95
|
+
return { type: 'object', ...result };
|
|
96
|
+
}
|
|
97
|
+
createLifecycleProxy() {
|
|
98
|
+
return {
|
|
99
|
+
getClientForFile: (filePath) => this.requireManager().getClientForFile(filePath),
|
|
100
|
+
getReadyClients: (language) => this.requireManager().getReadyClients(language),
|
|
101
|
+
getFileDiagnostics: (filePath) => this.requireManager().getFileDiagnostics(filePath),
|
|
102
|
+
getWorkspaceDiagnostics: (language) => this.requireManager().getWorkspaceDiagnostics(language),
|
|
103
|
+
getHealth: () => this.requireManager().getHealth(),
|
|
104
|
+
ensureLanguageForFile: async (filePath) => await this.requireManager().ensureLanguageForFile(filePath)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
requireManager() {
|
|
108
|
+
if (this.currentManager === null) {
|
|
109
|
+
throw new Error("No project root set. Call lsp_init({ root: '/path/to/project' }) first.");
|
|
110
|
+
}
|
|
111
|
+
return this.currentManager;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.McpServer = McpServer;
|
|
115
|
+
function isRecord(value) {
|
|
116
|
+
return typeof value === 'object' && value !== null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerReadTools = registerReadTools;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const formatters_1 = require("../formatters");
|
|
11
|
+
const uri_1 = require("../../utils/uri");
|
|
12
|
+
const shared_1 = require("./shared");
|
|
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() })
|
|
17
|
+
}, async (args) => {
|
|
18
|
+
return await handleInitTool(args, options);
|
|
19
|
+
});
|
|
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) => {
|
|
24
|
+
return await runFileRequest({
|
|
25
|
+
args,
|
|
26
|
+
lifecycleManager,
|
|
27
|
+
method: 'textDocument/definition',
|
|
28
|
+
timeoutMs: 5000,
|
|
29
|
+
format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
|
|
30
|
+
raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
registrar.registerTool('lsp_references', { description: 'Find references', inputSchema: positionSchema.extend({ includeDeclaration: zod_1.z.boolean().optional() }) }, async (args) => {
|
|
34
|
+
const includeDeclaration = args.includeDeclaration === true;
|
|
35
|
+
return await runFileRequest({
|
|
36
|
+
args,
|
|
37
|
+
lifecycleManager,
|
|
38
|
+
method: 'textDocument/references',
|
|
39
|
+
timeoutMs: 15000,
|
|
40
|
+
params: (uri, position) => ({ textDocument: { uri }, position, context: { includeDeclaration } }),
|
|
41
|
+
format: formatters_1.formatReferences,
|
|
42
|
+
raw: shared_1.normalizeLocations
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
registrar.registerTool('lsp_document_symbols', { description: 'List document symbols', inputSchema: zod_1.z.object({ file: zod_1.z.string() }) }, async (args) => {
|
|
46
|
+
return await runFileRequest({
|
|
47
|
+
args,
|
|
48
|
+
lifecycleManager,
|
|
49
|
+
method: 'textDocument/documentSymbol',
|
|
50
|
+
timeoutMs: 15000,
|
|
51
|
+
format: formatters_1.formatSymbols,
|
|
52
|
+
raw: shared_1.normalizeSymbols
|
|
53
|
+
});
|
|
54
|
+
});
|
|
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 : '';
|
|
57
|
+
const results = await Promise.all(lifecycleManager.getReadyClients().map(async (client) => {
|
|
58
|
+
return await client.request('workspace/symbol', { query }, 30000);
|
|
59
|
+
}));
|
|
60
|
+
const merged = results.flat().slice(0, query === '' ? 100 : 500);
|
|
61
|
+
return (0, shared_1.success)((0, formatters_1.formatSymbols)(merged), (0, shared_1.normalizeSymbols)(merged));
|
|
62
|
+
});
|
|
63
|
+
registrar.registerTool('lsp_completion', { description: 'Get completions', inputSchema: positionSchema }, async (args) => {
|
|
64
|
+
return await runFileRequest({
|
|
65
|
+
args,
|
|
66
|
+
lifecycleManager,
|
|
67
|
+
method: 'textDocument/completion',
|
|
68
|
+
timeoutMs: 5000,
|
|
69
|
+
format: (result) => (0, formatters_1.formatCompletion)(asCompletionItems(result)),
|
|
70
|
+
raw: (result) => asCompletionItems(result)?.slice(0, 50) ?? null
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
registrar.registerTool('lsp_diagnostics', { description: 'Show diagnostics', 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) => {
|
|
74
|
+
const scope = args.scope === 'workspace' ? 'workspace' : 'file';
|
|
75
|
+
if (scope === 'workspace') {
|
|
76
|
+
const language = typeof args.language === 'string' ? args.language : undefined;
|
|
77
|
+
const diagnostics = lifecycleManager.getWorkspaceDiagnostics(language).slice(0, 200);
|
|
78
|
+
return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, 'workspace'), diagnostics);
|
|
79
|
+
}
|
|
80
|
+
const filePath = typeof args.file === 'string' ? args.file : '';
|
|
81
|
+
const diagnostics = lifecycleManager.getFileDiagnostics(filePath);
|
|
82
|
+
return (0, shared_1.success)((0, formatters_1.formatDiagnostics)(diagnostics, 'file'), diagnostics);
|
|
83
|
+
});
|
|
84
|
+
registrar.registerTool('lsp_signature_help', { description: 'Show signature help', inputSchema: positionSchema }, async (args) => {
|
|
85
|
+
return await runFileRequest({ args, lifecycleManager, method: 'textDocument/signatureHelp', timeoutMs: 5000, format: stringifyResult, raw: (result) => result });
|
|
86
|
+
});
|
|
87
|
+
registrar.registerTool('lsp_type_definition', { description: 'Find type definitions', inputSchema: positionSchema }, async (args) => {
|
|
88
|
+
return await runFileRequest({
|
|
89
|
+
args,
|
|
90
|
+
lifecycleManager,
|
|
91
|
+
method: 'textDocument/typeDefinition',
|
|
92
|
+
timeoutMs: 5000,
|
|
93
|
+
format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
|
|
94
|
+
raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
registrar.registerTool('lsp_implementation', { description: 'Find implementations', inputSchema: positionSchema }, async (args) => {
|
|
98
|
+
return await runFileRequest({
|
|
99
|
+
args,
|
|
100
|
+
lifecycleManager,
|
|
101
|
+
method: 'textDocument/implementation',
|
|
102
|
+
timeoutMs: 5000,
|
|
103
|
+
format: (result) => (0, formatters_1.formatDefinition)(asLocationArray(result)),
|
|
104
|
+
raw: (result) => (0, shared_1.normalizeLocations)(asLocationArray(result))
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
registrar.registerTool('lsp_health', { description: 'Show LSP server health', inputSchema: zod_1.z.object({}) }, async () => {
|
|
108
|
+
const health = lifecycleManager.getHealth();
|
|
109
|
+
return (0, shared_1.success)((0, formatters_1.formatHealth)(health), health);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function handleInitTool(args, options) {
|
|
113
|
+
const root = typeof args.root === 'string' ? args.root : '';
|
|
114
|
+
if (root.length === 0) {
|
|
115
|
+
return (0, shared_1.failure)('Project root is required. Provide lsp_init({ root: \'/absolute/path\' }).');
|
|
116
|
+
}
|
|
117
|
+
if (!node_path_1.default.isAbsolute(root)) {
|
|
118
|
+
return (0, shared_1.failure)(`Project root must be an absolute path: ${root}`);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const stats = await (0, promises_1.stat)(root);
|
|
122
|
+
if (!stats.isDirectory()) {
|
|
123
|
+
return (0, shared_1.failure)(`Project root is not a directory: ${root}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return (0, shared_1.failure)(`Project root does not exist: ${root}`);
|
|
128
|
+
}
|
|
129
|
+
const languages = Array.isArray(args.languages)
|
|
130
|
+
? args.languages.filter((item) => typeof item === 'string')
|
|
131
|
+
: undefined;
|
|
132
|
+
try {
|
|
133
|
+
const initialized = await options.initializeManager(root, languages);
|
|
134
|
+
const languageNames = initialized.health.map((entry) => formatLanguageName(entry.language));
|
|
135
|
+
const started = initialized.health.filter((entry) => entry.status === 'ready').length;
|
|
136
|
+
const errors = initialized.health.filter((entry) => entry.status === 'error').length;
|
|
137
|
+
const lazyNote = languageNames.length === 0 ? ' Language servers will start on first file access.' : '';
|
|
138
|
+
const text = `Initialized LSP for ${initialized.root}. Detected languages: ${formatLanguageList(languageNames)}. LSP servers: ${started} started, ${errors} errors.${lazyNote}`;
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: 'text', text }],
|
|
141
|
+
text,
|
|
142
|
+
raw: {
|
|
143
|
+
root: initialized.root,
|
|
144
|
+
languages: languageNames,
|
|
145
|
+
health: initialized.health
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return (0, shared_1.mapToolError)(error, 30);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const positionSchema = zod_1.z.object({
|
|
154
|
+
file: zod_1.z.string(),
|
|
155
|
+
line: zod_1.z.number().int(),
|
|
156
|
+
character: zod_1.z.number().int()
|
|
157
|
+
});
|
|
158
|
+
async function runFileRequest(options) {
|
|
159
|
+
const filePath = typeof options.args.file === 'string' ? options.args.file : '';
|
|
160
|
+
await options.lifecycleManager.ensureLanguageForFile(filePath);
|
|
161
|
+
const client = options.lifecycleManager.getClientForFile(filePath);
|
|
162
|
+
if (!client) {
|
|
163
|
+
return (0, shared_1.noServerResult)(filePath);
|
|
164
|
+
}
|
|
165
|
+
const position = {
|
|
166
|
+
line: Number(options.args.line ?? 0),
|
|
167
|
+
character: Number(options.args.character ?? 0)
|
|
168
|
+
};
|
|
169
|
+
const uri = (0, uri_1.pathToUri)(filePath);
|
|
170
|
+
try {
|
|
171
|
+
await (0, shared_1.ensureDidOpen)(client, filePath);
|
|
172
|
+
const result = await client.request(options.method, options.params?.(uri, position) ?? {
|
|
173
|
+
textDocument: { uri },
|
|
174
|
+
position
|
|
175
|
+
}, options.timeoutMs);
|
|
176
|
+
return (0, shared_1.success)(options.format(result), options.raw(result));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return (0, shared_1.mapToolError)(error, options.timeoutMs / 1000);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function asLocationArray(result) {
|
|
183
|
+
if (!result) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return Array.isArray(result) ? result : [result];
|
|
187
|
+
}
|
|
188
|
+
function asCompletionItems(result) {
|
|
189
|
+
if (!result) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return Array.isArray(result) ? result : result.items;
|
|
193
|
+
}
|
|
194
|
+
function stringifyResult(result) {
|
|
195
|
+
if (!result) {
|
|
196
|
+
return 'No result';
|
|
197
|
+
}
|
|
198
|
+
return JSON.stringify(result, null, 2);
|
|
199
|
+
}
|
|
200
|
+
function formatLanguageList(languages) {
|
|
201
|
+
return languages.length === 0 ? 'None' : languages.join(', ');
|
|
202
|
+
}
|
|
203
|
+
function formatLanguageName(language) {
|
|
204
|
+
if (language.length === 0) {
|
|
205
|
+
return language;
|
|
206
|
+
}
|
|
207
|
+
return `${language[0]?.toUpperCase() ?? ''}${language.slice(1)}`;
|
|
208
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.success = success;
|
|
7
|
+
exports.failure = failure;
|
|
8
|
+
exports.ensureDidOpen = ensureDidOpen;
|
|
9
|
+
exports.clearOpenedFiles = clearOpenedFiles;
|
|
10
|
+
exports.noServerResult = noServerResult;
|
|
11
|
+
exports.noProjectRootResult = noProjectRootResult;
|
|
12
|
+
exports.mapToolError = mapToolError;
|
|
13
|
+
exports.normalizeLocations = normalizeLocations;
|
|
14
|
+
exports.normalizeSymbols = normalizeSymbols;
|
|
15
|
+
const promises_1 = require("node:fs/promises");
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const uri_1 = require("../../utils/uri");
|
|
18
|
+
const openedFiles = new Set();
|
|
19
|
+
function success(text, raw) {
|
|
20
|
+
return { content: [{ type: 'text', text }], raw };
|
|
21
|
+
}
|
|
22
|
+
function failure(text, raw = null) {
|
|
23
|
+
return { content: [{ type: 'text', text }], error: true, raw };
|
|
24
|
+
}
|
|
25
|
+
async function ensureDidOpen(client, filePath) {
|
|
26
|
+
if (openedFiles.has(filePath)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const text = await (0, promises_1.readFile)(filePath, 'utf8');
|
|
30
|
+
client.notify('textDocument/didOpen', {
|
|
31
|
+
textDocument: {
|
|
32
|
+
uri: (0, uri_1.pathToUri)(filePath),
|
|
33
|
+
languageId: languageIdForFile(filePath),
|
|
34
|
+
version: 1,
|
|
35
|
+
text
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
openedFiles.add(filePath);
|
|
39
|
+
}
|
|
40
|
+
function clearOpenedFiles() {
|
|
41
|
+
openedFiles.clear();
|
|
42
|
+
}
|
|
43
|
+
function noServerResult(filePath) {
|
|
44
|
+
return failure(`No language server available for ${node_path_1.default.extname(filePath) || 'unknown'} files. Run lsp_health for details.`);
|
|
45
|
+
}
|
|
46
|
+
function noProjectRootResult() {
|
|
47
|
+
const text = "No project root set. Call lsp_init({ root: '/path/to/project' }) first.";
|
|
48
|
+
return { content: [{ type: 'text', text }], text, error: true, raw: null };
|
|
49
|
+
}
|
|
50
|
+
function mapToolError(error, timeoutSeconds) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
if (message.includes('timed out')) {
|
|
53
|
+
return failure(`Operation timed out after ${timeoutSeconds}s — try a more specific query or check the LSP server health`);
|
|
54
|
+
}
|
|
55
|
+
if (message.includes('LSP server exited')) {
|
|
56
|
+
return failure('Der Language Server ist neu gestartet, bitte versuche es erneut.');
|
|
57
|
+
}
|
|
58
|
+
return failure(message, error);
|
|
59
|
+
}
|
|
60
|
+
function normalizeLocations(locations) {
|
|
61
|
+
if (!locations || locations.length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return locations.map((location) => ({ path: (0, uri_1.uriToPath)(location.uri), range: location.range }));
|
|
65
|
+
}
|
|
66
|
+
function normalizeSymbols(symbols) {
|
|
67
|
+
if (!symbols || symbols.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const normalized = [];
|
|
71
|
+
for (const symbol of symbols) {
|
|
72
|
+
if ('location' in symbol) {
|
|
73
|
+
normalized.push({
|
|
74
|
+
name: symbol.name,
|
|
75
|
+
kind: symbol.kind,
|
|
76
|
+
path: (0, uri_1.uriToPath)(symbol.location.uri),
|
|
77
|
+
range: symbol.location.range
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
normalized.push({ name: symbol.name, kind: symbol.kind, range: symbol.range, selectionRange: symbol.selectionRange });
|
|
82
|
+
}
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
function languageIdForFile(filePath) {
|
|
86
|
+
const ext = node_path_1.default.extname(filePath).toLowerCase();
|
|
87
|
+
const languageMap = {
|
|
88
|
+
'.ts': 'typescript',
|
|
89
|
+
'.tsx': 'typescriptreact',
|
|
90
|
+
'.js': 'javascript',
|
|
91
|
+
'.jsx': 'javascriptreact',
|
|
92
|
+
'.py': 'python',
|
|
93
|
+
'.go': 'go',
|
|
94
|
+
'.rs': 'rust',
|
|
95
|
+
'.java': 'java',
|
|
96
|
+
'.cs': 'csharp',
|
|
97
|
+
'.php': 'php',
|
|
98
|
+
'.rb': 'ruby',
|
|
99
|
+
'.kt': 'kotlin',
|
|
100
|
+
'.swift': 'swift',
|
|
101
|
+
'.c': 'c',
|
|
102
|
+
'.cpp': 'cpp',
|
|
103
|
+
'.h': 'c'
|
|
104
|
+
};
|
|
105
|
+
return languageMap[ext] ?? 'plaintext';
|
|
106
|
+
}
|