@theupsider/lsp-mcp 0.1.1 → 0.1.3

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.
@@ -5,8 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.LspClient = void 0;
7
7
  const node_events_1 = require("node:events");
8
+ const promises_1 = require("node:fs/promises");
8
9
  const node_child_process_1 = require("node:child_process");
9
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const language_registry_1 = require("../detection/language-registry");
10
12
  const uri_1 = require("../utils/uri");
11
13
  class LspClient extends node_events_1.EventEmitter {
12
14
  serverDef;
@@ -21,6 +23,7 @@ class LspClient extends node_events_1.EventEmitter {
21
23
  exitExpected = false;
22
24
  initializeResult = null;
23
25
  forcedKillTimer = null;
26
+ openedFiles = new Set();
24
27
  constructor(serverDef, projectRoot, logLevel) {
25
28
  super();
26
29
  this.serverDef = serverDef;
@@ -34,35 +37,46 @@ class LspClient extends node_events_1.EventEmitter {
34
37
  this.process = (0, node_child_process_1.spawn)(this.serverDef.cmd, this.serverDef.args, {
35
38
  cwd: this.projectRoot,
36
39
  env: { ...process.env },
37
- stdio: 'pipe'
40
+ stdio: "pipe",
38
41
  });
39
- this.process.stdout.on('data', (chunk) => {
42
+ this.process.stdout.on("data", (chunk) => {
40
43
  this.handleData(chunk);
41
44
  });
42
- this.process.on('error', (error) => {
43
- this.log('error', 'lsp_process_error', { error: error.message });
44
- this.emit('error', error);
45
+ this.process.on("error", (error) => {
46
+ this.log("error", "lsp_process_error", { error: error.message });
47
+ this.emit("error", error);
45
48
  });
46
- this.process.on('exit', (code, signal) => {
49
+ this.process.on("exit", (code, signal) => {
47
50
  this.handleExit(code, signal);
48
51
  });
49
- this.log('info', 'lsp_starting', { language: this.serverDef.cmd });
50
- const initializeResult = await this.request('initialize', {
52
+ this.log("info", "lsp_starting", { language: this.serverDef.cmd });
53
+ const initializeResult = await this.request("initialize", {
51
54
  processId: process.pid,
52
- clientInfo: { name: 'lsp-mcp', version: '0.1.0' },
55
+ clientInfo: { name: "lsp-mcp", version: "0.1.0" },
53
56
  rootUri: (0, uri_1.pathToUri)(this.projectRoot),
54
57
  workspaceFolders: [
55
58
  {
56
59
  uri: (0, uri_1.pathToUri)(this.projectRoot),
57
- name: node_path_1.default.basename(this.projectRoot)
58
- }
60
+ name: node_path_1.default.basename(this.projectRoot),
61
+ },
59
62
  ],
60
- capabilities: {}
63
+ capabilities: {
64
+ textDocument: {
65
+ publishDiagnostics: {
66
+ relatedInformation: true,
67
+ versionSupport: false,
68
+ tagSupport: { valueSet: [1, 2] },
69
+ },
70
+ synchronization: {
71
+ didSave: true,
72
+ },
73
+ },
74
+ },
61
75
  }, 30000);
62
76
  this.initializeResult = initializeResult;
63
- this.notify('initialized', {});
77
+ this.notify("initialized", {});
64
78
  this.ready = true;
65
- this.log('info', 'lsp_ready', { language: this.serverDef.cmd });
79
+ this.log("info", "lsp_ready", { language: this.serverDef.cmd });
66
80
  }
67
81
  isReady() {
68
82
  return this.ready;
@@ -71,10 +85,10 @@ class LspClient extends node_events_1.EventEmitter {
71
85
  const id = this.nextRequestId;
72
86
  this.nextRequestId += 1;
73
87
  const message = {
74
- jsonrpc: '2.0',
88
+ jsonrpc: "2.0",
75
89
  id,
76
90
  method,
77
- params
91
+ params,
78
92
  };
79
93
  return await new Promise((resolve, reject) => {
80
94
  const timeoutHandle = setTimeout(() => {
@@ -84,16 +98,16 @@ class LspClient extends node_events_1.EventEmitter {
84
98
  this.pendingRequests.set(id, {
85
99
  resolve: (value) => resolve(value),
86
100
  reject,
87
- timeoutHandle
101
+ timeoutHandle,
88
102
  });
89
103
  this.sendMessage(message);
90
104
  });
91
105
  }
92
106
  notify(method, params) {
93
107
  const message = {
94
- jsonrpc: '2.0',
108
+ jsonrpc: "2.0",
95
109
  method,
96
- params
110
+ params,
97
111
  };
98
112
  this.sendMessage(message);
99
113
  }
@@ -103,65 +117,108 @@ class LspClient extends node_events_1.EventEmitter {
103
117
  this.ready = false;
104
118
  return;
105
119
  }
106
- await this.request('shutdown', {}, 5000);
107
- this.notify('exit', {});
120
+ await this.request("shutdown", {}, 5000);
121
+ this.notify("exit", {});
108
122
  this.ready = false;
109
123
  const currentProcess = this.process;
110
124
  this.forcedKillTimer = setTimeout(() => {
111
- currentProcess.kill('SIGKILL');
125
+ currentProcess.kill("SIGKILL");
112
126
  }, 5000);
113
127
  }
114
128
  getCapabilities() {
115
129
  return this.initializeResult?.capabilities ?? null;
116
130
  }
131
+ waitForDiagnosticsPublish(filePath, timeoutMs) {
132
+ const uri = (0, uri_1.pathToUri)(filePath);
133
+ return new Promise((resolve) => {
134
+ const timer = setTimeout(resolve, timeoutMs);
135
+ const handler = (method, params) => {
136
+ if (method === "textDocument/publishDiagnostics") {
137
+ const p = params;
138
+ if (p?.uri === uri) {
139
+ clearTimeout(timer);
140
+ this.off("notification", handler);
141
+ resolve();
142
+ }
143
+ }
144
+ };
145
+ this.on("notification", handler);
146
+ });
147
+ }
148
+ async ensureDidOpen(filePath) {
149
+ if (this.openedFiles.has(filePath)) {
150
+ return;
151
+ }
152
+ const text = await (0, promises_1.readFile)(filePath, "utf8");
153
+ this.notify("textDocument/didOpen", {
154
+ textDocument: {
155
+ uri: (0, uri_1.pathToUri)(filePath),
156
+ languageId: (0, language_registry_1.extensionToLanguageId)(node_path_1.default.extname(filePath)),
157
+ version: 1,
158
+ text,
159
+ },
160
+ });
161
+ this.openedFiles.add(filePath);
162
+ }
163
+ async ensureSeedFileOpen(extensions) {
164
+ if (this.openedFiles.size > 0) {
165
+ return;
166
+ }
167
+ const file = await findFirstFileWithExtension(this.projectRoot, extensions);
168
+ if (file) {
169
+ await this.ensureDidOpen(file);
170
+ }
171
+ }
117
172
  sendMessage(message) {
118
173
  if (!this.process) {
119
- throw new Error('LSP process is not running');
174
+ throw new Error("LSP process is not running");
120
175
  }
121
176
  const body = JSON.stringify(message);
122
- const content = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
177
+ const content = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
123
178
  this.process.stdin.write(content);
124
179
  }
125
180
  handleData(chunk) {
126
- const incoming = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
181
+ const incoming = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
127
182
  this.buffer = Buffer.concat([this.buffer, incoming]);
128
183
  while (true) {
129
184
  if (this.contentLength === null) {
130
- const headerEnd = this.buffer.indexOf('\r\n\r\n');
185
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
131
186
  if (headerEnd === -1) {
132
187
  return;
133
188
  }
134
- const header = this.buffer.subarray(0, headerEnd).toString('utf8');
189
+ const header = this.buffer.subarray(0, headerEnd).toString("utf8");
135
190
  this.buffer = this.buffer.subarray(headerEnd + 4);
136
191
  const contentLengthLine = header
137
- .split('\r\n')
138
- .find((line) => line.toLowerCase().startsWith('content-length:'));
192
+ .split("\r\n")
193
+ .find((line) => line.toLowerCase().startsWith("content-length:"));
139
194
  if (!contentLengthLine) {
140
- const error = new Error('Missing Content-Length header');
141
- this.emit('error', error);
195
+ const error = new Error("Missing Content-Length header");
196
+ this.emit("error", error);
142
197
  return;
143
198
  }
144
- const value = Number.parseInt(contentLengthLine.slice('Content-Length:'.length).trim(), 10);
199
+ const value = Number.parseInt(contentLengthLine.slice("Content-Length:".length).trim(), 10);
145
200
  this.contentLength = value;
146
201
  }
147
202
  if (this.buffer.byteLength < this.contentLength) {
148
203
  return;
149
204
  }
150
- const messageBody = this.buffer.subarray(0, this.contentLength).toString('utf8');
205
+ const messageBody = this.buffer
206
+ .subarray(0, this.contentLength)
207
+ .toString("utf8");
151
208
  this.buffer = this.buffer.subarray(this.contentLength);
152
209
  this.contentLength = null;
153
210
  this.handleMessage(JSON.parse(messageBody));
154
211
  }
155
212
  }
156
213
  handleMessage(message) {
157
- if (!message || typeof message !== 'object') {
214
+ if (!message || typeof message !== "object") {
158
215
  return;
159
216
  }
160
- if ('method' in message) {
161
- this.emit('notification', message.method, message.params);
217
+ if ("method" in message) {
218
+ this.emit("notification", message.method, message.params);
162
219
  return;
163
220
  }
164
- if ('id' in message && 'error' in message && message.id !== null) {
221
+ if ("id" in message && "error" in message && message.id !== null) {
165
222
  const pending = this.pendingRequests.get(message.id);
166
223
  if (!pending) {
167
224
  return;
@@ -171,7 +228,7 @@ class LspClient extends node_events_1.EventEmitter {
171
228
  pending.reject(new Error(message.error.message));
172
229
  return;
173
230
  }
174
- if ('id' in message && 'result' in message) {
231
+ if ("id" in message && "result" in message) {
175
232
  const pending = this.pendingRequests.get(message.id);
176
233
  if (!pending) {
177
234
  return;
@@ -186,13 +243,16 @@ class LspClient extends node_events_1.EventEmitter {
186
243
  clearTimeout(this.forcedKillTimer);
187
244
  this.forcedKillTimer = null;
188
245
  }
189
- this.rejectAllPending(new Error(`LSP server exited (code: ${code ?? 'null'}, signal: ${signal ?? 'null'})`));
246
+ this.rejectAllPending(new Error(`LSP server exited (code: ${code ?? "null"}, signal: ${signal ?? "null"})`));
190
247
  this.ready = false;
191
248
  this.process = null;
192
249
  if (!this.exitExpected) {
193
- const error = new Error(`LSP server exited unexpectedly (code: ${code ?? 'null'}, signal: ${signal ?? 'null'})`);
194
- this.log('error', 'lsp_crash', { error: error.message, language: this.serverDef.cmd });
195
- this.emit('crash', error);
250
+ const error = new Error(`LSP server exited unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "null"})`);
251
+ this.log("error", "lsp_crash", {
252
+ error: error.message,
253
+ language: this.serverDef.cmd,
254
+ });
255
+ this.emit("crash", error);
196
256
  }
197
257
  }
198
258
  rejectAllPending(error) {
@@ -210,7 +270,7 @@ class LspClient extends node_events_1.EventEmitter {
210
270
  timestamp: new Date().toISOString(),
211
271
  level,
212
272
  event,
213
- ...extra
273
+ ...extra,
214
274
  };
215
275
  process.stderr.write(`${JSON.stringify(payload)}\n`);
216
276
  }
@@ -220,7 +280,34 @@ function shouldLog(configuredLevel, messageLevel) {
220
280
  const order = {
221
281
  error: 0,
222
282
  info: 1,
223
- debug: 2
283
+ debug: 2,
224
284
  };
225
285
  return order[messageLevel] <= order[configuredLevel];
226
286
  }
287
+ const SKIP_DIRS = new Set([
288
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next',
289
+ 'coverage', '__pycache__', 'target', '.cache', 'vendor',
290
+ ]);
291
+ async function findFirstFileWithExtension(dir, extensions) {
292
+ const extensionSet = new Set(extensions.map((e) => e.toLowerCase()));
293
+ let entries;
294
+ try {
295
+ entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ for (const entry of entries) {
301
+ if (entry.isFile() && extensionSet.has(node_path_1.default.extname(entry.name).toLowerCase())) {
302
+ return node_path_1.default.join(dir, entry.name);
303
+ }
304
+ }
305
+ for (const entry of entries) {
306
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
307
+ const result = await findFirstFileWithExtension(node_path_1.default.join(dir, entry.name), extensions);
308
+ if (result)
309
+ return result;
310
+ }
311
+ }
312
+ return null;
313
+ }
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServerSupervisor = void 0;
4
+ const lsp_mapping_1 = require("../detection/lsp-mapping");
5
+ const installer_1 = require("./installer");
6
+ const lsp_client_1 = require("./lsp-client");
7
+ class ServerSupervisor {
8
+ language;
9
+ restartCount = 0;
10
+ client = null;
11
+ status = 'starting';
12
+ error = undefined;
13
+ capabilities = undefined;
14
+ serverDef = null;
15
+ healthInterval = null;
16
+ projectRoot;
17
+ logLevel;
18
+ onDiagnostics;
19
+ shuttingDown = false;
20
+ constructor(language, projectRoot, logLevel, onDiagnostics) {
21
+ this.language = language;
22
+ this.projectRoot = projectRoot;
23
+ this.logLevel = logLevel;
24
+ this.onDiagnostics = onDiagnostics;
25
+ }
26
+ async start() {
27
+ const serverDef = await this.resolveServer();
28
+ if (!serverDef) {
29
+ this.status = 'error';
30
+ this.error = `No LSP server available for ${this.language}`;
31
+ return;
32
+ }
33
+ this.serverDef = serverDef;
34
+ const client = new lsp_client_1.LspClient(serverDef, this.projectRoot, this.logLevel);
35
+ this.client = client;
36
+ client.on('crash', async () => {
37
+ if (this.shuttingDown) {
38
+ return;
39
+ }
40
+ await this.restart(`LSP server crashed for ${this.language}`);
41
+ });
42
+ client.on('error', (error) => {
43
+ this.status = 'error';
44
+ this.error = error instanceof Error ? error.message : 'Unknown LSP error';
45
+ });
46
+ client.on('notification', (method, params) => {
47
+ this.onDiagnostics(method, params);
48
+ });
49
+ try {
50
+ await client.start();
51
+ this.status = 'ready';
52
+ this.error = undefined;
53
+ this.capabilities = client.getCapabilities() ?? undefined;
54
+ this.startHealthChecks();
55
+ }
56
+ catch (error) {
57
+ this.status = 'error';
58
+ this.error = error instanceof Error ? error.message : 'Unknown LSP startup error';
59
+ }
60
+ }
61
+ async restart(reason) {
62
+ if (this.healthInterval) {
63
+ clearInterval(this.healthInterval);
64
+ this.healthInterval = null;
65
+ }
66
+ if (this.restartCount >= 3) {
67
+ this.status = 'error';
68
+ this.error = reason;
69
+ this.client = null;
70
+ return;
71
+ }
72
+ this.restartCount += 1;
73
+ this.status = 'starting';
74
+ this.error = reason;
75
+ if (this.client) {
76
+ try {
77
+ await promiseWithTimeout(this.client.shutdown(), 5000, 'LSP shutdown timed out');
78
+ }
79
+ catch {
80
+ // Ignore shutdown failures during restart.
81
+ }
82
+ }
83
+ this.client = null;
84
+ await this.start();
85
+ }
86
+ async shutdown() {
87
+ this.shuttingDown = true;
88
+ if (this.healthInterval) {
89
+ clearInterval(this.healthInterval);
90
+ this.healthInterval = null;
91
+ }
92
+ if (this.client) {
93
+ await this.client.shutdown();
94
+ this.client = null;
95
+ }
96
+ }
97
+ getClient() {
98
+ return this.client;
99
+ }
100
+ getHealth() {
101
+ return {
102
+ language: this.language,
103
+ status: this.status,
104
+ error: this.error,
105
+ capabilities: this.capabilities
106
+ };
107
+ }
108
+ startHealthChecks() {
109
+ if (this.healthInterval) {
110
+ clearInterval(this.healthInterval);
111
+ }
112
+ this.healthInterval = setInterval(() => {
113
+ void this.runHealthCheck();
114
+ }, 30000);
115
+ }
116
+ async runHealthCheck() {
117
+ if (!this.client || this.status !== 'ready') {
118
+ return;
119
+ }
120
+ if (!this.client.isReady()) {
121
+ await this.restart('LSP client lost ready state');
122
+ }
123
+ }
124
+ async resolveServer() {
125
+ const available = await (0, lsp_mapping_1.findAvailableLsp)(this.language);
126
+ if (available) {
127
+ return available;
128
+ }
129
+ const candidate = (0, lsp_mapping_1.getLspCandidates)(this.language)[0] ?? null;
130
+ if (!candidate) {
131
+ return null;
132
+ }
133
+ const installation = await (0, installer_1.installLsp)(candidate);
134
+ if (!installation.success) {
135
+ return null;
136
+ }
137
+ return candidate;
138
+ }
139
+ }
140
+ exports.ServerSupervisor = ServerSupervisor;
141
+ async function promiseWithTimeout(promise, timeoutMs, message) {
142
+ return await new Promise((resolve, reject) => {
143
+ const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
144
+ promise.then((value) => {
145
+ clearTimeout(timer);
146
+ resolve(value);
147
+ }, (error) => {
148
+ clearTimeout(timer);
149
+ reject(error);
150
+ });
151
+ });
152
+ }
@@ -133,7 +133,7 @@ describe('mcp formatters', () => {
133
133
  expect((0, formatters_1.formatDefinition)(null)).toBe('No result');
134
134
  expect((0, formatters_1.formatReferences)([])).toBe('No result');
135
135
  expect((0, formatters_1.formatSymbols)(null)).toBe('No result');
136
- expect((0, formatters_1.formatDiagnostics)([], 'file')).toBe('No result');
136
+ expect((0, formatters_1.formatDiagnostics)([], 'file')).toBe('No diagnostics found');
137
137
  expect((0, formatters_1.formatCompletion)(null)).toBe('No result');
138
138
  expect((0, formatters_1.formatHealth)([])).toBe('No result');
139
139
  });
@@ -4,7 +4,6 @@ const promises_1 = require("node:fs/promises");
4
4
  const read_tools_1 = require("../tools/read-tools");
5
5
  const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
6
6
  jest.mock('node:fs/promises', () => ({
7
- readFile: jest.fn(),
8
7
  stat: jest.fn()
9
8
  }));
10
9
  class FakeRegistrar {
@@ -16,7 +15,6 @@ class FakeRegistrar {
16
15
  describe('registerReadTools', () => {
17
16
  beforeEach(() => {
18
17
  jest.clearAllMocks();
19
- promises_1.readFile.mockResolvedValue('const foo = 1;');
20
18
  promises_1.stat.mockResolvedValue({ isDirectory: () => true });
21
19
  });
22
20
  it('initializes LSP with a valid root and reports health', async () => {
@@ -94,10 +92,8 @@ describe('registerReadTools', () => {
94
92
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
95
93
  const hover = await getHandler(registrar, 'lsp_hover')({ file: '/workspace/src/index.ts', line: 2, character: 4 });
96
94
  await getHandler(registrar, 'lsp_hover')({ file: '/workspace/src/index.ts', line: 2, character: 4 });
97
- expect(promises_1.readFile).toHaveBeenCalledTimes(1);
98
- expect(client.notify).toHaveBeenCalledWith('textDocument/didOpen', expect.objectContaining({
99
- textDocument: expect.objectContaining({ uri: 'file:///workspace/src/index.ts', text: 'const foo = 1;' })
100
- }));
95
+ expect(client.ensureDidOpen).toHaveBeenCalledTimes(2);
96
+ expect(client.ensureDidOpen).toHaveBeenCalledWith('/workspace/src/index.ts');
101
97
  expect(client.request).toHaveBeenCalledWith('textDocument/hover', {
102
98
  textDocument: { uri: 'file:///workspace/src/index.ts' },
103
99
  position: { line: 2, character: 4 }
@@ -143,8 +139,10 @@ describe('registerReadTools', () => {
143
139
  const registrar = new FakeRegistrar();
144
140
  const firstClient = createClient([{ name: 'UserService', kind: 5, location: { uri: 'file:///workspace/src/user.ts', range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } } } }]);
145
141
  const secondClient = createClient([{ name: 'login', kind: 12, location: { uri: 'file:///workspace/src/auth.ts', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 3 } } } }]);
146
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ workspaceClients: [firstClient, secondClient] }), { initializeManager: jest.fn() });
142
+ const lifecycle = createLifecycle({ workspaceClients: [firstClient, secondClient] });
143
+ (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
147
144
  const result = await getHandler(registrar, 'lsp_workspace_symbols')({ query: 'log' });
145
+ expect(lifecycle.ensureSeedFilesOpen).toHaveBeenCalledTimes(1);
148
146
  expect(firstClient.request).toHaveBeenCalledWith('workspace/symbol', { query: 'log' }, 30000);
149
147
  expect(secondClient.request).toHaveBeenCalledWith('workspace/symbol', { query: 'log' }, 30000);
150
148
  expect(result).toEqual({
@@ -267,7 +265,8 @@ function createLifecycle(options) {
267
265
  getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === 'file:///workspace/src/index.ts')),
268
266
  getWorkspaceDiagnostics: jest.fn((_) => options.diagnostics ?? []),
269
267
  getHealth: jest.fn(() => options.health ?? []),
270
- ensureLanguageForFile: jest.fn().mockResolvedValue(undefined)
268
+ ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
269
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
271
270
  };
272
271
  }
273
272
  function createClient(result) {
@@ -276,6 +275,9 @@ function createClient(result) {
276
275
  ? jest.fn().mockRejectedValue(result)
277
276
  : jest.fn().mockResolvedValue(result),
278
277
  notify: jest.fn(),
279
- getCapabilities: jest.fn(() => ({ renameProvider: true }))
278
+ getCapabilities: jest.fn(() => ({ renameProvider: true })),
279
+ ensureDidOpen: jest.fn().mockResolvedValue(undefined),
280
+ waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
281
+ ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
280
282
  };
281
283
  }
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockRegisterCapabilities = jest.fn();
3
4
  const mockSetRequestHandler = jest.fn();
4
5
  const mockConnect = jest.fn().mockResolvedValue(undefined);
5
6
  jest.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
6
7
  McpServer: jest.fn().mockImplementation(() => ({
7
8
  connect: mockConnect,
8
9
  server: {
10
+ registerCapabilities: mockRegisterCapabilities,
9
11
  setRequestHandler: mockSetRequestHandler
10
12
  }
11
13
  }))
@@ -32,9 +34,10 @@ describe('McpServer', () => {
32
34
  beforeEach(() => {
33
35
  jest.clearAllMocks();
34
36
  });
35
- it('registers request handlers and connects on start', async () => {
37
+ it('registers tool capabilities, request handlers, and connects on start', async () => {
36
38
  const server = new server_1.McpServer('info');
37
39
  await server.start();
40
+ expect(mockRegisterCapabilities).toHaveBeenCalledWith({ tools: {} });
38
41
  expect(mockSetRequestHandler).toHaveBeenCalledTimes(2);
39
42
  expect(mockConnect).toHaveBeenCalledWith({ kind: 'stdio' });
40
43
  });
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const promises_1 = require("node:fs/promises");
4
- const shared_1 = require("../tools/shared");
5
4
  const write_tools_1 = require("../tools/write-tools");
6
5
  jest.mock('node:fs/promises', () => ({
7
6
  access: jest.fn(),
@@ -17,7 +16,6 @@ class FakeRegistrar {
17
16
  describe('registerWriteTools', () => {
18
17
  beforeEach(() => {
19
18
  jest.clearAllMocks();
20
- (0, shared_1.clearOpenedFiles)();
21
19
  promises_1.access.mockResolvedValue(undefined);
22
20
  promises_1.readFile.mockImplementation(async (filePath) => {
23
21
  const pathText = String(filePath);
@@ -165,7 +163,8 @@ describe('registerWriteTools', () => {
165
163
  getFileDiagnostics: jest.fn((_) => []),
166
164
  getWorkspaceDiagnostics: jest.fn(() => []),
167
165
  getHealth: jest.fn(() => []),
168
- ensureLanguageForFile: jest.fn().mockResolvedValue(undefined)
166
+ ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
167
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
169
168
  });
170
169
  await expect(getHandler(registrar, 'lsp_apply_workspace_edit')({ edit: {} })).resolves.toEqual({
171
170
  content: [{ type: 'text', text: 'No language servers are ready. Run lsp_health for details.' }],
@@ -181,7 +180,8 @@ describe('registerWriteTools', () => {
181
180
  getFileDiagnostics: jest.fn((_) => []),
182
181
  getWorkspaceDiagnostics: jest.fn(() => []),
183
182
  getHealth: jest.fn(() => []),
184
- ensureLanguageForFile: jest.fn().mockResolvedValue(undefined)
183
+ ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
184
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
185
185
  };
186
186
  (0, write_tools_1.registerWriteTools)(registrar, noClientLifecycle);
187
187
  await expect(getHandler(registrar, 'lsp_rename')({ file: '/workspace/README.md', line: 0, character: 0, newName: 'x' })).resolves.toEqual({
@@ -237,7 +237,8 @@ function createLifecycle(client) {
237
237
  getFileDiagnostics: jest.fn((_) => []),
238
238
  getWorkspaceDiagnostics: jest.fn(() => []),
239
239
  getHealth: jest.fn(() => []),
240
- ensureLanguageForFile: jest.fn().mockResolvedValue(undefined)
240
+ ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
241
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
241
242
  };
242
243
  }
243
244
  function createClient(result, capabilities = { renameProvider: true }) {
@@ -252,6 +253,9 @@ function createClient(result, capabilities = { renameProvider: true }) {
252
253
  return result;
253
254
  }),
254
255
  notify: jest.fn(),
255
- getCapabilities: jest.fn(() => capabilities)
256
+ getCapabilities: jest.fn(() => capabilities),
257
+ ensureDidOpen: jest.fn().mockResolvedValue(undefined),
258
+ waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
259
+ ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
256
260
  };
257
261
  }
@@ -116,7 +116,7 @@ function formatSymbols(symbols) {
116
116
  }
117
117
  function formatDiagnostics(diagnostics, scope) {
118
118
  if (!diagnostics || diagnostics.length === 0) {
119
- return 'No result';
119
+ return scope === 'workspace' ? 'No diagnostics found in workspace' : 'No diagnostics found';
120
120
  }
121
121
  const grouped = new Map();
122
122
  const ordered = [...diagnostics].sort((left, right) => (left.severity ?? 4) - (right.severity ?? 4));
@@ -28,8 +28,7 @@ class McpServer {
28
28
  const registrar = {
29
29
  registerTool: (name, config, handler) => {
30
30
  this.tools.set(name, { name, description: config.description, inputSchema: config.inputSchema, handler });
31
- },
32
- fromJsonSchema: (schema) => schema
31
+ }
33
32
  };
34
33
  (0, read_tools_1.registerReadTools)(registrar, lifecycleProxy, { initializeManager: async (root, languages) => await this.initializeManager(root, languages) });
35
34
  (0, write_tools_1.registerWriteTools)(registrar, lifecycleProxy);
@@ -64,6 +63,7 @@ class McpServer {
64
63
  }
65
64
  }
66
65
  configureToolHandlers() {
66
+ this.server.server.registerCapabilities({ tools: {} });
67
67
  this.server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: this.listTools() }));
68
68
  this.server.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
69
69
  const toolName = request.params.name;
@@ -101,7 +101,8 @@ class McpServer {
101
101
  getFileDiagnostics: (filePath) => this.requireManager().getFileDiagnostics(filePath),
102
102
  getWorkspaceDiagnostics: (language) => this.requireManager().getWorkspaceDiagnostics(language),
103
103
  getHealth: () => this.requireManager().getHealth(),
104
- ensureLanguageForFile: async (filePath) => await this.requireManager().ensureLanguageForFile(filePath)
104
+ ensureLanguageForFile: async (filePath) => await this.requireManager().ensureLanguageForFile(filePath),
105
+ ensureSeedFilesOpen: async () => await this.requireManager().ensureSeedFilesOpen()
105
106
  };
106
107
  }
107
108
  requireManager() {