@theupsider/lsp-mcp 1.0.1 → 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
package/dist/mcp/formatters.js
CHANGED
|
@@ -10,78 +10,78 @@ exports.formatHealth = formatHealth;
|
|
|
10
10
|
exports.formatError = formatError;
|
|
11
11
|
const uri_1 = require("../utils/uri");
|
|
12
12
|
const SYMBOL_KIND_ICONS = {
|
|
13
|
-
1:
|
|
14
|
-
2:
|
|
15
|
-
3:
|
|
16
|
-
4:
|
|
17
|
-
5:
|
|
18
|
-
6:
|
|
19
|
-
7:
|
|
20
|
-
8:
|
|
21
|
-
9:
|
|
22
|
-
10:
|
|
23
|
-
11:
|
|
24
|
-
12:
|
|
25
|
-
13:
|
|
26
|
-
14:
|
|
27
|
-
15:
|
|
28
|
-
16:
|
|
29
|
-
17:
|
|
30
|
-
18:
|
|
31
|
-
19:
|
|
32
|
-
20:
|
|
33
|
-
21:
|
|
34
|
-
22:
|
|
35
|
-
23:
|
|
36
|
-
24:
|
|
37
|
-
25:
|
|
38
|
-
26:
|
|
13
|
+
1: "📄",
|
|
14
|
+
2: "📦",
|
|
15
|
+
3: "🔖",
|
|
16
|
+
4: "🧩",
|
|
17
|
+
5: "📦",
|
|
18
|
+
6: "🔧",
|
|
19
|
+
7: "🏗️",
|
|
20
|
+
8: "🧠",
|
|
21
|
+
9: "📐",
|
|
22
|
+
10: "📚",
|
|
23
|
+
11: "🔌",
|
|
24
|
+
12: "ƒ",
|
|
25
|
+
13: "≡",
|
|
26
|
+
14: "🔒",
|
|
27
|
+
15: "📝",
|
|
28
|
+
16: "№",
|
|
29
|
+
17: "🧮",
|
|
30
|
+
18: "📏",
|
|
31
|
+
19: "🧱",
|
|
32
|
+
20: "🔑",
|
|
33
|
+
21: "❌",
|
|
34
|
+
22: "🧩",
|
|
35
|
+
23: "➡️",
|
|
36
|
+
24: "🎯",
|
|
37
|
+
25: "📦",
|
|
38
|
+
26: "🔎",
|
|
39
39
|
};
|
|
40
40
|
const COMPLETION_KIND_LABELS = {
|
|
41
|
-
2:
|
|
42
|
-
3:
|
|
43
|
-
4:
|
|
44
|
-
5:
|
|
45
|
-
6:
|
|
46
|
-
7:
|
|
47
|
-
8:
|
|
48
|
-
9:
|
|
49
|
-
10:
|
|
50
|
-
11:
|
|
51
|
-
12:
|
|
52
|
-
13:
|
|
53
|
-
14:
|
|
54
|
-
15:
|
|
55
|
-
16:
|
|
56
|
-
17:
|
|
57
|
-
18:
|
|
58
|
-
19:
|
|
59
|
-
20:
|
|
60
|
-
21:
|
|
61
|
-
22:
|
|
62
|
-
23:
|
|
63
|
-
24:
|
|
64
|
-
25:
|
|
41
|
+
2: "Methods",
|
|
42
|
+
3: "Functions",
|
|
43
|
+
4: "Constructors",
|
|
44
|
+
5: "Fields",
|
|
45
|
+
6: "Variables",
|
|
46
|
+
7: "Classes",
|
|
47
|
+
8: "Interfaces",
|
|
48
|
+
9: "Modules",
|
|
49
|
+
10: "Properties",
|
|
50
|
+
11: "Units",
|
|
51
|
+
12: "Values",
|
|
52
|
+
13: "Enums",
|
|
53
|
+
14: "Keywords",
|
|
54
|
+
15: "Snippets",
|
|
55
|
+
16: "Colors",
|
|
56
|
+
17: "Files",
|
|
57
|
+
18: "References",
|
|
58
|
+
19: "Folders",
|
|
59
|
+
20: "Enum members",
|
|
60
|
+
21: "Constants",
|
|
61
|
+
22: "Structs",
|
|
62
|
+
23: "Events",
|
|
63
|
+
24: "Operators",
|
|
64
|
+
25: "Type parameters",
|
|
65
65
|
};
|
|
66
66
|
const DIAGNOSTIC_SEVERITY_LABELS = {
|
|
67
|
-
1:
|
|
68
|
-
2:
|
|
69
|
-
3:
|
|
70
|
-
4:
|
|
67
|
+
1: "Errors",
|
|
68
|
+
2: "Warnings",
|
|
69
|
+
3: "Information",
|
|
70
|
+
4: "Hints",
|
|
71
71
|
};
|
|
72
72
|
function formatHover(result) {
|
|
73
73
|
if (!result) {
|
|
74
|
-
return
|
|
74
|
+
return "No result";
|
|
75
75
|
}
|
|
76
76
|
const rawText = hoverContentsToText(result.contents).trim();
|
|
77
77
|
if (!rawText) {
|
|
78
|
-
return
|
|
78
|
+
return "No result";
|
|
79
79
|
}
|
|
80
80
|
const codeBlocks = Array.from(rawText.matchAll(/```(?<lang>[^\n`]*)\n(?<code>[\s\S]*?)```/g));
|
|
81
81
|
const firstCode = codeBlocks[0]?.groups?.code?.trim();
|
|
82
|
-
const firstLang = codeBlocks[0]?.groups?.lang?.trim() ??
|
|
83
|
-
const summarySource = rawText.replace(/```[\s\S]*?```/g,
|
|
84
|
-
const summaryLine = summarySource.split(/\n+/).find(Boolean) ??
|
|
82
|
+
const firstLang = codeBlocks[0]?.groups?.lang?.trim() ?? "";
|
|
83
|
+
const summarySource = rawText.replace(/```[\s\S]*?```/g, "").trim();
|
|
84
|
+
const summaryLine = summarySource.split(/\n+/).find(Boolean) ?? "";
|
|
85
85
|
if (firstCode && summaryLine) {
|
|
86
86
|
return `**${firstCode}** — ${summaryLine}\n\n\`\`\`${firstLang}\n${firstCode}\n\`\`\``;
|
|
87
87
|
}
|
|
@@ -89,34 +89,44 @@ function formatHover(result) {
|
|
|
89
89
|
}
|
|
90
90
|
function formatDefinition(locations) {
|
|
91
91
|
if (!locations || locations.length === 0) {
|
|
92
|
-
return
|
|
92
|
+
return "No result";
|
|
93
93
|
}
|
|
94
94
|
if (locations.length === 1) {
|
|
95
95
|
return `Found 1 definition: \`${formatLocation(locations[0])}\``;
|
|
96
96
|
}
|
|
97
|
-
return [
|
|
97
|
+
return [
|
|
98
|
+
"Found definitions:",
|
|
99
|
+
...locations.map((location) => `- \`${formatLocation(location)}\``),
|
|
100
|
+
].join("\n");
|
|
98
101
|
}
|
|
99
102
|
function formatReferences(locations) {
|
|
100
103
|
if (!locations || locations.length === 0) {
|
|
101
|
-
return
|
|
104
|
+
return "No result";
|
|
102
105
|
}
|
|
103
|
-
return [
|
|
106
|
+
return [
|
|
107
|
+
`Found ${locations.length} references:`,
|
|
108
|
+
...locations.map((location) => `- \`${formatLocation(location)}\``),
|
|
109
|
+
].join("\n");
|
|
104
110
|
}
|
|
105
111
|
function formatSymbols(symbols) {
|
|
106
112
|
if (!symbols || symbols.length === 0) {
|
|
107
|
-
return
|
|
113
|
+
return "No result";
|
|
108
114
|
}
|
|
109
|
-
return symbols
|
|
110
|
-
|
|
111
|
-
const
|
|
115
|
+
return symbols
|
|
116
|
+
.map((symbol) => {
|
|
117
|
+
const icon = SYMBOL_KIND_ICONS[symbol.kind] ?? "•";
|
|
118
|
+
const detail = "location" in symbol
|
|
112
119
|
? formatLocation(symbol.location)
|
|
113
120
|
: `${symbol.name}`;
|
|
114
|
-
return `- ${icon} \`${symbol.name}\`${
|
|
115
|
-
})
|
|
121
|
+
return `- ${icon} \`${symbol.name}\`${"location" in symbol ? ` — ${detail}` : ""}`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n");
|
|
116
124
|
}
|
|
117
125
|
function formatDiagnostics(diagnostics, scope) {
|
|
118
126
|
if (!diagnostics || diagnostics.length === 0) {
|
|
119
|
-
return scope ===
|
|
127
|
+
return scope === "workspace"
|
|
128
|
+
? "No diagnostics found in workspace"
|
|
129
|
+
: "No diagnostics found";
|
|
120
130
|
}
|
|
121
131
|
const grouped = new Map();
|
|
122
132
|
const ordered = [...diagnostics].sort((left, right) => (left.severity ?? 4) - (right.severity ?? 4));
|
|
@@ -126,77 +136,85 @@ function formatDiagnostics(diagnostics, scope) {
|
|
|
126
136
|
bucket.push(diagnostic);
|
|
127
137
|
grouped.set(severity, bucket);
|
|
128
138
|
}
|
|
129
|
-
const lines = [
|
|
139
|
+
const lines = [
|
|
140
|
+
`${scope === "workspace" ? "Workspace" : "File"} diagnostics: ${diagnostics.length} issue(s)`,
|
|
141
|
+
];
|
|
130
142
|
for (const severity of [1, 2, 3, 4]) {
|
|
131
143
|
const bucket = grouped.get(severity);
|
|
132
144
|
if (!bucket || bucket.length === 0) {
|
|
133
145
|
continue;
|
|
134
146
|
}
|
|
135
|
-
lines.push(
|
|
147
|
+
lines.push("", `### ${DIAGNOSTIC_SEVERITY_LABELS[severity]}`);
|
|
136
148
|
for (const diagnostic of bucket) {
|
|
137
149
|
lines.push(`- ${formatDiagnostic(diagnostic)}`);
|
|
138
150
|
}
|
|
139
151
|
}
|
|
140
|
-
return lines.join(
|
|
152
|
+
return lines.join("\n");
|
|
141
153
|
}
|
|
142
154
|
function formatCompletion(items) {
|
|
143
155
|
if (!items || items.length === 0) {
|
|
144
|
-
return
|
|
156
|
+
return "No result";
|
|
145
157
|
}
|
|
146
158
|
const limited = items.slice(0, 50);
|
|
147
159
|
const grouped = new Map();
|
|
148
160
|
for (const item of limited) {
|
|
149
|
-
const label = COMPLETION_KIND_LABELS[item.kind ?? 1] ??
|
|
161
|
+
const label = COMPLETION_KIND_LABELS[item.kind ?? 1] ?? "Other";
|
|
150
162
|
const bucket = grouped.get(label) ?? [];
|
|
151
163
|
bucket.push(item);
|
|
152
164
|
grouped.set(label, bucket);
|
|
153
165
|
}
|
|
154
|
-
const lines = [
|
|
166
|
+
const lines = [
|
|
167
|
+
`Showing ${limited.length} of ${items.length} completion item(s)`,
|
|
168
|
+
];
|
|
155
169
|
for (const [label, bucket] of grouped.entries()) {
|
|
156
|
-
lines.push(
|
|
170
|
+
lines.push("", `### ${label}`);
|
|
157
171
|
for (const item of bucket) {
|
|
158
|
-
lines.push(`- \`${item.label}\`${item.detail ? ` — ${item.detail}` :
|
|
172
|
+
lines.push(`- \`${item.label}\`${item.detail ? ` — ${item.detail}` : ""}`);
|
|
159
173
|
}
|
|
160
174
|
}
|
|
161
|
-
return lines.join(
|
|
175
|
+
return lines.join("\n");
|
|
162
176
|
}
|
|
163
177
|
function formatHealth(healths) {
|
|
164
178
|
if (healths.length === 0) {
|
|
165
|
-
return
|
|
179
|
+
return "No result";
|
|
166
180
|
}
|
|
167
181
|
return [
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
...healths.map((health) => `| ${health.language} | ${health.status} | ${health.error ??
|
|
171
|
-
].join(
|
|
182
|
+
"| Language | Status | Error |",
|
|
183
|
+
"| --- | --- | --- |",
|
|
184
|
+
...healths.map((health) => `| ${health.language} | ${health.status} | ${health.error ?? ""} |`),
|
|
185
|
+
].join("\n");
|
|
172
186
|
}
|
|
173
187
|
function formatError(error) {
|
|
174
188
|
return {
|
|
175
189
|
error: true,
|
|
176
190
|
text: error instanceof Error ? error.message : String(error),
|
|
177
|
-
raw: error
|
|
191
|
+
raw: error,
|
|
178
192
|
};
|
|
179
193
|
}
|
|
180
194
|
function hoverContentsToText(contents) {
|
|
181
|
-
if (typeof contents ===
|
|
195
|
+
if (typeof contents === "string") {
|
|
182
196
|
return contents;
|
|
183
197
|
}
|
|
184
198
|
if (Array.isArray(contents)) {
|
|
185
|
-
return contents.map(markedStringToText).join(
|
|
199
|
+
return contents.map(markedStringToText).join("\n\n");
|
|
186
200
|
}
|
|
187
|
-
if (
|
|
201
|
+
if ("kind" in contents) {
|
|
188
202
|
return contents.value;
|
|
189
203
|
}
|
|
190
204
|
return markedStringToText(contents);
|
|
191
205
|
}
|
|
192
206
|
function markedStringToText(value) {
|
|
193
|
-
return typeof value ===
|
|
207
|
+
return typeof value === "string"
|
|
208
|
+
? value
|
|
209
|
+
: `\`\`\`${value.language}\n${value.value}\n\`\`\``;
|
|
194
210
|
}
|
|
195
211
|
function formatLocation(location) {
|
|
196
212
|
return `${(0, uri_1.uriToPath)(location.uri)}:${location.range.start.line + 1}:${location.range.start.character + 1}`;
|
|
197
213
|
}
|
|
198
214
|
function formatDiagnostic(diagnostic) {
|
|
199
|
-
const location = diagnostic.uri
|
|
200
|
-
|
|
215
|
+
const location = diagnostic.uri
|
|
216
|
+
? `\`${(0, uri_1.uriToPath)(diagnostic.uri)}:${diagnostic.range.start.line + 1}:${diagnostic.range.start.character + 1}\` `
|
|
217
|
+
: "";
|
|
218
|
+
const source = diagnostic.source ? `${diagnostic.source}: ` : "";
|
|
201
219
|
return `${location}${source}${diagnostic.message}`.trim();
|
|
202
220
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -18,19 +18,29 @@ class McpServer {
|
|
|
18
18
|
createLifecycleManager;
|
|
19
19
|
server;
|
|
20
20
|
tools = new Map();
|
|
21
|
-
constructor(logLevel =
|
|
21
|
+
constructor(logLevel = "info", createLifecycleManager = (root, level) => new lifecycle_manager_1.LifecycleManager(root, level)) {
|
|
22
22
|
this.logLevel = logLevel;
|
|
23
23
|
this.createLifecycleManager = createLifecycleManager;
|
|
24
|
-
this.server = new mcp_js_1.McpServer({
|
|
24
|
+
this.server = new mcp_js_1.McpServer({
|
|
25
|
+
name: "@theupsider/lsp-mcp",
|
|
26
|
+
version: "0.1.0",
|
|
27
|
+
});
|
|
25
28
|
}
|
|
26
29
|
async start() {
|
|
27
30
|
const lifecycleProxy = this.createLifecycleProxy();
|
|
28
31
|
const registrar = {
|
|
29
32
|
registerTool: (name, config, handler) => {
|
|
30
|
-
this.tools.set(name, {
|
|
31
|
-
|
|
33
|
+
this.tools.set(name, {
|
|
34
|
+
name,
|
|
35
|
+
description: config.description,
|
|
36
|
+
inputSchema: config.inputSchema,
|
|
37
|
+
handler,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
32
40
|
};
|
|
33
|
-
(0, read_tools_1.registerReadTools)(registrar, lifecycleProxy, {
|
|
41
|
+
(0, read_tools_1.registerReadTools)(registrar, lifecycleProxy, {
|
|
42
|
+
initializeManager: async (root, languages) => await this.initializeManager(root, languages),
|
|
43
|
+
});
|
|
34
44
|
(0, write_tools_1.registerWriteTools)(registrar, lifecycleProxy);
|
|
35
45
|
this.configureToolHandlers();
|
|
36
46
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
@@ -64,13 +74,15 @@ class McpServer {
|
|
|
64
74
|
}
|
|
65
75
|
configureToolHandlers() {
|
|
66
76
|
this.server.server.registerCapabilities({ tools: {} });
|
|
67
|
-
this.server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
77
|
+
this.server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
78
|
+
tools: this.listTools(),
|
|
79
|
+
}));
|
|
68
80
|
this.server.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
69
81
|
const toolName = request.params.name;
|
|
70
|
-
if (toolName ===
|
|
71
|
-
return (0, shared_1.failure)(`Already initialized at ${this.currentRoot ??
|
|
82
|
+
if (toolName === "lsp_init" && this.initialized) {
|
|
83
|
+
return (0, shared_1.failure)(`Already initialized at ${this.currentRoot ?? "unknown"}. Call lsp_init again with a new root to switch projects.`);
|
|
72
84
|
}
|
|
73
|
-
if (toolName !==
|
|
85
|
+
if (toolName !== "lsp_init" && !this.initialized) {
|
|
74
86
|
return (0, shared_1.noProjectRootResult)();
|
|
75
87
|
}
|
|
76
88
|
const tool = this.tools.get(toolName);
|
|
@@ -84,15 +96,18 @@ class McpServer {
|
|
|
84
96
|
return [...this.tools.values()].map((tool) => ({
|
|
85
97
|
name: tool.name,
|
|
86
98
|
description: tool.description,
|
|
87
|
-
inputSchema: this.toInputSchema(tool.inputSchema)
|
|
99
|
+
inputSchema: this.toInputSchema(tool.inputSchema),
|
|
88
100
|
}));
|
|
89
101
|
}
|
|
90
102
|
toInputSchema(schema) {
|
|
91
103
|
const normalized = (0, zod_compat_js_1.normalizeObjectSchema)(schema);
|
|
92
104
|
const result = normalized
|
|
93
|
-
? (0, zod_json_schema_compat_js_1.toJsonSchemaCompat)(normalized, {
|
|
105
|
+
? (0, zod_json_schema_compat_js_1.toJsonSchemaCompat)(normalized, {
|
|
106
|
+
strictUnions: true,
|
|
107
|
+
pipeStrategy: "input",
|
|
108
|
+
})
|
|
94
109
|
: {};
|
|
95
|
-
return { type:
|
|
110
|
+
return { type: "object", ...result };
|
|
96
111
|
}
|
|
97
112
|
createLifecycleProxy() {
|
|
98
113
|
return {
|
|
@@ -102,7 +117,8 @@ class McpServer {
|
|
|
102
117
|
getWorkspaceDiagnostics: (language) => this.requireManager().getWorkspaceDiagnostics(language),
|
|
103
118
|
getHealth: () => this.requireManager().getHealth(),
|
|
104
119
|
ensureLanguageForFile: async (filePath) => await this.requireManager().ensureLanguageForFile(filePath),
|
|
105
|
-
ensureSeedFilesOpen: async () => await this.requireManager().ensureSeedFilesOpen()
|
|
120
|
+
ensureSeedFilesOpen: async () => await this.requireManager().ensureSeedFilesOpen(),
|
|
121
|
+
analyzeWorkspace: async (language) => await this.requireManager().analyzeWorkspace(language),
|
|
106
122
|
};
|
|
107
123
|
}
|
|
108
124
|
requireManager() {
|
|
@@ -114,5 +130,5 @@ class McpServer {
|
|
|
114
130
|
}
|
|
115
131
|
exports.McpServer = McpServer;
|
|
116
132
|
function isRecord(value) {
|
|
117
|
-
return typeof value ===
|
|
133
|
+
return typeof value === "object" && value !== null;
|
|
118
134
|
}
|