@theupsider/lsp-mcp 1.0.8 → 1.0.9

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.
@@ -2,27 +2,42 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const mockRegisterCapabilities = jest.fn();
4
4
  const mockSetRequestHandler = jest.fn();
5
+ const mockSetNotificationHandler = jest.fn();
5
6
  const mockConnect = jest.fn().mockResolvedValue(undefined);
6
- jest.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
7
+ const mockGetClientCapabilities = jest.fn().mockReturnValue(undefined);
8
+ const mockListRoots = jest.fn().mockResolvedValue({ roots: [] });
9
+ const mockSendToolListChanged = jest.fn().mockResolvedValue(undefined);
10
+ const mockInnerServer = {
11
+ registerCapabilities: mockRegisterCapabilities,
12
+ setRequestHandler: mockSetRequestHandler,
13
+ setNotificationHandler: mockSetNotificationHandler,
14
+ getClientCapabilities: mockGetClientCapabilities,
15
+ listRoots: mockListRoots,
16
+ sendToolListChanged: mockSendToolListChanged,
17
+ oninitialized: undefined,
18
+ };
19
+ jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
7
20
  McpServer: jest.fn().mockImplementation(() => ({
8
21
  connect: mockConnect,
9
- server: {
10
- registerCapabilities: mockRegisterCapabilities,
11
- setRequestHandler: mockSetRequestHandler
12
- }
13
- }))
22
+ server: mockInnerServer,
23
+ })),
14
24
  }));
15
- jest.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
16
- StdioServerTransport: jest.fn().mockImplementation(() => ({ kind: 'stdio' }))
25
+ jest.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
26
+ StdioServerTransport: jest.fn().mockImplementation(() => ({ kind: "stdio" })),
17
27
  }));
18
- jest.mock('@modelcontextprotocol/sdk/server/zod-compat.js', () => ({
19
- normalizeObjectSchema: jest.fn().mockReturnValue(null)
28
+ jest.mock("@modelcontextprotocol/sdk/server/zod-compat.js", () => ({
29
+ normalizeObjectSchema: jest.fn().mockReturnValue(null),
20
30
  }));
21
- jest.mock('@modelcontextprotocol/sdk/server/zod-json-schema-compat.js', () => ({
22
- toJsonSchemaCompat: jest.fn().mockReturnValue({})
31
+ jest.mock("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js", () => ({
32
+ toJsonSchemaCompat: jest.fn().mockReturnValue({}),
33
+ }));
34
+ jest.mock("../tools/read-tools", () => ({ registerReadTools: jest.fn() }));
35
+ jest.mock("../tools/write-tools", () => ({ registerWriteTools: jest.fn() }));
36
+ jest.mock("../../utils/workspace-config", () => ({
37
+ loadWorkspaceConfig: jest.fn().mockResolvedValue(null),
38
+ saveWorkspaceConfig: jest.fn().mockResolvedValue(undefined),
39
+ loadLastRoot: jest.fn().mockResolvedValue(null),
23
40
  }));
24
- jest.mock('../tools/read-tools', () => ({ registerReadTools: jest.fn() }));
25
- jest.mock('../tools/write-tools', () => ({ registerWriteTools: jest.fn() }));
26
41
  const server_1 = require("../server");
27
42
  function getHandler(schema) {
28
43
  const call = mockSetRequestHandler.mock.calls.find(([s]) => s === schema);
@@ -30,114 +45,240 @@ function getHandler(schema) {
30
45
  throw new Error(`No handler registered for schema`);
31
46
  return call[1];
32
47
  }
33
- describe('McpServer', () => {
48
+ describe("McpServer", () => {
34
49
  beforeEach(() => {
35
50
  jest.clearAllMocks();
51
+ mockInnerServer.oninitialized = undefined;
52
+ mockGetClientCapabilities.mockReturnValue(undefined);
53
+ mockListRoots.mockResolvedValue({ roots: [] });
54
+ mockSendToolListChanged.mockResolvedValue(undefined);
36
55
  });
37
- it('registers tool capabilities, request handlers, and connects on start', async () => {
38
- const server = new server_1.McpServer('info');
56
+ it("registers tool capabilities, request handlers, notification handler, and connects on start", async () => {
57
+ const server = new server_1.McpServer("info");
39
58
  await server.start();
40
59
  expect(mockRegisterCapabilities).toHaveBeenCalledWith({ tools: {} });
41
60
  expect(mockSetRequestHandler).toHaveBeenCalledTimes(2);
42
- expect(mockConnect).toHaveBeenCalledWith({ kind: 'stdio' });
61
+ expect(mockSetNotificationHandler).toHaveBeenCalledTimes(1);
62
+ expect(mockConnect).toHaveBeenCalledWith({ kind: "stdio" });
63
+ });
64
+ it("sets oninitialized callback on start", async () => {
65
+ const server = new server_1.McpServer("info");
66
+ await server.start();
67
+ expect(mockInnerServer.oninitialized).toBeInstanceOf(Function);
43
68
  });
44
- it('lists all registered tools regardless of init state', async () => {
45
- const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
46
- const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
69
+ it("lists lsp_init before auto-init", async () => {
70
+ const readTools = jest.requireMock("../tools/read-tools")
71
+ .registerReadTools;
72
+ const writeTools = jest.requireMock("../tools/write-tools")
73
+ .registerWriteTools;
47
74
  readTools.mockImplementationOnce((registrar) => {
48
- registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
49
- registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'definition' }], raw: null }));
75
+ registrar.registerTool("lsp_init", { description: "init" }, async () => ({ content: [{ type: "text", text: "ok" }], raw: null }));
76
+ registrar.registerTool("lsp_definition", { description: "definition" }, async () => ({
77
+ content: [{ type: "text", text: "definition" }],
78
+ raw: null,
79
+ }));
50
80
  });
51
81
  writeTools.mockImplementationOnce(() => undefined);
52
- const { ListToolsRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
53
- const server = new server_1.McpServer('info');
82
+ const { ListToolsRequestSchema } = jest.requireActual("@modelcontextprotocol/sdk/types.js");
83
+ const server = new server_1.McpServer("info");
54
84
  await server.start();
55
85
  const listHandler = getHandler(ListToolsRequestSchema);
56
86
  const result = await listHandler({});
57
- expect(result.tools.map((t) => t.name)).toContain('lsp_init');
58
- expect(result.tools.map((t) => t.name)).toContain('lsp_definition');
87
+ expect(result.tools.map((t) => t.name)).toContain("lsp_init");
88
+ expect(result.tools.map((t) => t.name)).toContain("lsp_definition");
59
89
  });
60
- it('lists all tools after initialization too', async () => {
61
- const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
62
- const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
90
+ it("hides lsp_init after successful auto-init (all servers ready)", async () => {
91
+ const readTools = jest.requireMock("../tools/read-tools")
92
+ .registerReadTools;
93
+ const writeTools = jest.requireMock("../tools/write-tools")
94
+ .registerWriteTools;
63
95
  readTools.mockImplementationOnce((registrar) => {
64
- registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
65
- registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'definition' }], raw: null }));
96
+ registrar.registerTool("lsp_init", { description: "init" }, async () => ({ content: [{ type: "text", text: "ok" }], raw: null }));
97
+ registrar.registerTool("lsp_definition", { description: "definition" }, async () => ({
98
+ content: [{ type: "text", text: "definition" }],
99
+ raw: null,
100
+ }));
66
101
  });
67
102
  writeTools.mockImplementationOnce(() => undefined);
68
- const { ListToolsRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
69
- const server = new server_1.McpServer('info');
103
+ const health = [{ language: "typescript", status: "ready" }];
104
+ const manager = {
105
+ start: jest.fn().mockResolvedValue(undefined),
106
+ shutdown: jest.fn().mockResolvedValue(undefined),
107
+ getHealth: jest.fn().mockReturnValue(health),
108
+ };
109
+ const factory = jest.fn().mockReturnValue(manager);
110
+ mockGetClientCapabilities.mockReturnValue({ roots: { listChanged: true } });
111
+ mockListRoots.mockResolvedValue({
112
+ roots: [{ uri: "file:///workspace/project" }],
113
+ });
114
+ const { ListToolsRequestSchema } = jest.requireActual("@modelcontextprotocol/sdk/types.js");
115
+ const server = new server_1.McpServer("info", factory);
70
116
  await server.start();
71
- server.setManager({});
117
+ // Trigger the oninitialized callback
118
+ await mockInnerServer.oninitialized();
119
+ const listHandler = getHandler(ListToolsRequestSchema);
120
+ const result = await listHandler({});
121
+ expect(result.tools.map((t) => t.name)).not.toContain("lsp_init");
122
+ expect(result.tools.map((t) => t.name)).toContain("lsp_definition");
123
+ });
124
+ it("keeps lsp_init visible after auto-init when a server has errors", async () => {
125
+ const readTools = jest.requireMock("../tools/read-tools")
126
+ .registerReadTools;
127
+ const writeTools = jest.requireMock("../tools/write-tools")
128
+ .registerWriteTools;
129
+ readTools.mockImplementationOnce((registrar) => {
130
+ registrar.registerTool("lsp_init", { description: "init" }, async () => ({ content: [{ type: "text", text: "ok" }], raw: null }));
131
+ });
132
+ writeTools.mockImplementationOnce(() => undefined);
133
+ const health = [
134
+ { language: "typescript", status: "ready" },
135
+ { language: "python", status: "error", error: "pylsp not found" },
136
+ ];
137
+ const manager = {
138
+ start: jest.fn().mockResolvedValue(undefined),
139
+ shutdown: jest.fn().mockResolvedValue(undefined),
140
+ getHealth: jest.fn().mockReturnValue(health),
141
+ };
142
+ const factory = jest.fn().mockReturnValue(manager);
143
+ mockGetClientCapabilities.mockReturnValue({ roots: { listChanged: true } });
144
+ mockListRoots.mockResolvedValue({
145
+ roots: [{ uri: "file:///workspace/project" }],
146
+ });
147
+ const { ListToolsRequestSchema } = jest.requireActual("@modelcontextprotocol/sdk/types.js");
148
+ const server = new server_1.McpServer("info", factory);
149
+ await server.start();
150
+ await mockInnerServer.oninitialized();
72
151
  const listHandler = getHandler(ListToolsRequestSchema);
73
152
  const result = await listHandler({});
74
- expect(result.tools.map((t) => t.name)).toContain('lsp_init');
75
- expect(result.tools.map((t) => t.name)).toContain('lsp_definition');
153
+ expect(result.tools.map((t) => t.name)).toContain("lsp_init");
76
154
  });
77
- it('returns no-root error for non-init tools before lsp_init', async () => {
78
- const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
79
- const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
155
+ it("falls back to persisted last root when client has no roots capability", async () => {
156
+ const { loadLastRoot, loadWorkspaceConfig } = jest.requireMock("../../utils/workspace-config");
157
+ loadLastRoot.mockResolvedValueOnce("/persisted/root");
158
+ loadWorkspaceConfig.mockResolvedValueOnce({ languages: ["typescript"] });
159
+ const health = [{ language: "typescript", status: "ready" }];
160
+ const manager = {
161
+ start: jest.fn().mockResolvedValue(undefined),
162
+ shutdown: jest.fn().mockResolvedValue(undefined),
163
+ getHealth: jest.fn().mockReturnValue(health),
164
+ };
165
+ const factory = jest.fn().mockReturnValue(manager);
166
+ mockGetClientCapabilities.mockReturnValue(undefined); // no roots capability
167
+ const server = new server_1.McpServer("info", factory);
168
+ await server.start();
169
+ await mockInnerServer.oninitialized();
170
+ expect(manager.start).toHaveBeenCalled();
171
+ expect(server.isInitToolHidden()).toBe(true);
172
+ });
173
+ it("returns no-root error for non-init tools before initialization", async () => {
174
+ const readTools = jest.requireMock("../tools/read-tools")
175
+ .registerReadTools;
176
+ const writeTools = jest.requireMock("../tools/write-tools")
177
+ .registerWriteTools;
80
178
  readTools.mockImplementationOnce((registrar) => {
81
- registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'reachable' }], raw: null }));
179
+ registrar.registerTool("lsp_definition", { description: "definition" }, async () => ({
180
+ content: [{ type: "text", text: "reachable" }],
181
+ raw: null,
182
+ }));
82
183
  });
83
184
  writeTools.mockImplementationOnce(() => undefined);
84
- const { CallToolRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
85
- const server = new server_1.McpServer('info');
185
+ const { CallToolRequestSchema } = jest.requireActual("@modelcontextprotocol/sdk/types.js");
186
+ const server = new server_1.McpServer("info");
86
187
  await server.start();
87
188
  const callHandler = getHandler(CallToolRequestSchema);
88
- const result = await callHandler({ params: { name: 'lsp_definition', arguments: {} } });
189
+ const result = await callHandler({
190
+ params: { name: "lsp_definition", arguments: {} },
191
+ });
89
192
  expect(result.content[0].text).toMatch(/No project root set/);
90
193
  });
91
- it('re-initializes with new root when lsp_init called again', async () => {
92
- const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
93
- const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
194
+ it("allows calling lsp_init directly even after initialization (re-init)", async () => {
195
+ const readTools = jest.requireMock("../tools/read-tools")
196
+ .registerReadTools;
197
+ const writeTools = jest.requireMock("../tools/write-tools")
198
+ .registerWriteTools;
94
199
  readTools.mockImplementationOnce((registrar) => {
95
- registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
200
+ registrar.registerTool("lsp_init", { description: "init" }, async () => ({
201
+ content: [{ type: "text", text: "initialized" }],
202
+ raw: null,
203
+ }));
96
204
  });
97
205
  writeTools.mockImplementationOnce(() => undefined);
98
- const { CallToolRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
99
- const server = new server_1.McpServer('info');
206
+ const { CallToolRequestSchema } = jest.requireActual("@modelcontextprotocol/sdk/types.js");
207
+ const server = new server_1.McpServer("info");
100
208
  await server.start();
101
209
  server.setManager({});
102
210
  const callHandler = getHandler(CallToolRequestSchema);
103
- const result = await callHandler({ params: { name: 'lsp_init', arguments: { root: '/x' } } });
104
- expect(result.content[0].text).toMatch(/Already initialized/);
211
+ const result = await callHandler({
212
+ params: { name: "lsp_init", arguments: { root: "/x" } },
213
+ });
214
+ expect(result.content[0].text).toBe("initialized");
105
215
  });
106
- it('shuts down the old manager before starting a new one', async () => {
216
+ it("shuts down the old manager before starting a new one", async () => {
107
217
  const order = [];
108
218
  const firstManager = {
109
219
  start: jest.fn().mockResolvedValue(undefined),
110
- shutdown: jest.fn().mockImplementation(async () => { order.push('shutdown-first'); }),
111
- getHealth: jest.fn().mockReturnValue([{ language: 'typescript', status: 'ready' }])
220
+ shutdown: jest.fn().mockImplementation(async () => {
221
+ order.push("shutdown-first");
222
+ }),
223
+ getHealth: jest
224
+ .fn()
225
+ .mockReturnValue([{ language: "typescript", status: "ready" }]),
112
226
  };
113
227
  const secondManager = {
114
- start: jest.fn().mockImplementation(async () => { order.push('start-second'); }),
228
+ start: jest.fn().mockImplementation(async () => {
229
+ order.push("start-second");
230
+ }),
115
231
  shutdown: jest.fn().mockResolvedValue(undefined),
116
- getHealth: jest.fn().mockReturnValue([{ language: 'python', status: 'ready' }])
232
+ getHealth: jest
233
+ .fn()
234
+ .mockReturnValue([{ language: "python", status: "ready" }]),
117
235
  };
118
- const factory = jest.fn()
236
+ const factory = jest
237
+ .fn()
119
238
  .mockReturnValueOnce(firstManager)
120
239
  .mockReturnValueOnce(secondManager);
121
- const server = new server_1.McpServer('debug', factory);
122
- await expect(server.initializeManager('/workspace-one')).resolves.toEqual({
123
- root: '/workspace-one',
124
- health: [{ language: 'typescript', status: 'ready' }]
240
+ const server = new server_1.McpServer("debug", factory);
241
+ await expect(server.initializeManager("/workspace-one")).resolves.toEqual({
242
+ root: "/workspace-one",
243
+ health: [{ language: "typescript", status: "ready" }],
125
244
  });
126
- await expect(server.initializeManager('/workspace-two')).resolves.toEqual({
127
- root: '/workspace-two',
128
- health: [{ language: 'python', status: 'ready' }]
245
+ await expect(server.initializeManager("/workspace-two")).resolves.toEqual({
246
+ root: "/workspace-two",
247
+ health: [{ language: "python", status: "ready" }],
129
248
  });
130
- expect(order).toEqual(['shutdown-first', 'start-second']);
249
+ expect(order).toEqual(["shutdown-first", "start-second"]);
131
250
  });
132
- it('tolerates shutdown with no active manager', async () => {
133
- const server = new server_1.McpServer('info');
251
+ it("tolerates shutdown with no active manager", async () => {
252
+ const server = new server_1.McpServer("info");
134
253
  await expect(server.shutdown()).resolves.toBeUndefined();
135
254
  });
136
- it('shuts down active manager on server shutdown', async () => {
137
- const manager = { shutdown: jest.fn().mockResolvedValue(undefined) };
138
- const server = new server_1.McpServer('info');
255
+ it("shuts down active manager on server shutdown", async () => {
256
+ const manager = {
257
+ shutdown: jest.fn().mockResolvedValue(undefined),
258
+ };
259
+ const server = new server_1.McpServer("info");
139
260
  server.setManager(manager);
140
261
  await server.shutdown();
141
262
  expect(manager.shutdown).toHaveBeenCalledTimes(1);
142
263
  });
264
+ it("sends tool list changed notification when roots change", async () => {
265
+ const { RootsListChangedNotificationSchema } = jest.requireActual("@modelcontextprotocol/sdk/types.js");
266
+ const health = [{ language: "typescript", status: "ready" }];
267
+ const manager = {
268
+ start: jest.fn().mockResolvedValue(undefined),
269
+ shutdown: jest.fn().mockResolvedValue(undefined),
270
+ getHealth: jest.fn().mockReturnValue(health),
271
+ };
272
+ const factory = jest.fn().mockReturnValue(manager);
273
+ mockGetClientCapabilities.mockReturnValue({ roots: {} });
274
+ mockListRoots.mockResolvedValue({
275
+ roots: [{ uri: "file:///workspace/new-project" }],
276
+ });
277
+ const server = new server_1.McpServer("info", factory);
278
+ await server.start();
279
+ const notificationHandler = mockSetNotificationHandler.mock.calls.find(([s]) => s === RootsListChangedNotificationSchema)?.[1];
280
+ expect(notificationHandler).toBeDefined();
281
+ await notificationHandler({});
282
+ expect(mockSendToolListChanged).toHaveBeenCalled();
283
+ });
143
284
  });
@@ -8,12 +8,15 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
8
8
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
9
9
  const lifecycle_manager_1 = require("../lsp/lifecycle-manager");
10
10
  const shared_1 = require("./tools/shared");
11
+ const workspace_config_1 = require("../utils/workspace-config");
11
12
  const read_tools_1 = require("./tools/read-tools");
12
13
  const write_tools_1 = require("./tools/write-tools");
13
14
  class McpServer {
14
15
  currentManager = null;
15
16
  currentRoot = null;
16
17
  initialized = false;
18
+ /** When true, lsp_init is hidden from ListTools (client auto-initialized). */
19
+ hideInitTool = false;
17
20
  logLevel;
18
21
  createLifecycleManager;
19
22
  server;
@@ -39,10 +42,24 @@ class McpServer {
39
42
  },
40
43
  };
41
44
  (0, read_tools_1.registerReadTools)(registrar, lifecycleProxy, {
42
- initializeManager: async (root, languages) => await this.initializeManager(root, languages),
45
+ initializeManager: async (root, languages) => {
46
+ const result = await this.initializeManager(root, languages);
47
+ // When lsp_init is called explicitly, hide it on success, re-expose on error
48
+ this.updateInitVisibility(result.health);
49
+ await this.server.server.sendToolListChanged();
50
+ return result;
51
+ },
43
52
  });
44
53
  (0, write_tools_1.registerWriteTools)(registrar, lifecycleProxy);
45
54
  this.configureToolHandlers();
55
+ // Hook into post-handshake to attempt auto-init from roots or persisted config.
56
+ // Cast to `() => void` to satisfy the SDK type while still returning the promise
57
+ // so test harnesses can await it when needed.
58
+ this.server.server.oninitialized = (() => this.tryAutoInit());
59
+ // Handle roots/list_changed: client switched workspace
60
+ this.server.server.setNotificationHandler(types_js_1.RootsListChangedNotificationSchema, async () => {
61
+ await this.tryAutoInitFromRoots();
62
+ });
46
63
  const transport = new stdio_js_1.StdioServerTransport();
47
64
  await this.server.connect(transport);
48
65
  }
@@ -68,10 +85,69 @@ class McpServer {
68
85
  this.currentManager = null;
69
86
  this.currentRoot = null;
70
87
  this.initialized = false;
88
+ this.hideInitTool = false;
71
89
  if (activeManager) {
72
90
  await activeManager.shutdown();
73
91
  }
74
92
  }
93
+ /** True when lsp_init is currently hidden from the tool list. */
94
+ isInitToolHidden() {
95
+ return this.hideInitTool;
96
+ }
97
+ updateInitVisibility(health) {
98
+ const hasErrors = health.some((entry) => entry.status === "error");
99
+ this.hideInitTool = !hasErrors;
100
+ }
101
+ async tryAutoInit() {
102
+ // Prefer roots from the client if supported
103
+ const caps = this.server.server.getClientCapabilities();
104
+ if (caps?.roots) {
105
+ await this.tryAutoInitFromRoots();
106
+ return;
107
+ }
108
+ // Fallback: load the last-used root from persisted config
109
+ try {
110
+ const lastRoot = await (0, workspace_config_1.loadLastRoot)();
111
+ if (lastRoot) {
112
+ await this.autoInitRoot(lastRoot);
113
+ }
114
+ }
115
+ catch {
116
+ // Non-fatal; lsp_init stays visible
117
+ }
118
+ }
119
+ async tryAutoInitFromRoots() {
120
+ try {
121
+ const { roots } = await this.server.server.listRoots();
122
+ const firstUri = roots[0]?.uri;
123
+ if (!firstUri?.startsWith("file://"))
124
+ return;
125
+ // file:///path/to/project → /path/to/project
126
+ const root = decodeURIComponent(new URL(firstUri).pathname);
127
+ await this.autoInitRoot(root);
128
+ }
129
+ catch {
130
+ // Client may not support roots (e.g. Cursor bug) — stay visible
131
+ }
132
+ }
133
+ async autoInitRoot(root) {
134
+ const saved = await (0, workspace_config_1.loadWorkspaceConfig)(root);
135
+ const languages = saved?.languages;
136
+ const result = await this.initializeManager(root, languages);
137
+ this.updateInitVisibility(result.health);
138
+ // Persist successful init so we can restore on next startup
139
+ const readyLanguages = result.health
140
+ .filter((e) => e.status === "ready")
141
+ .map((e) => e.language);
142
+ await (0, workspace_config_1.saveWorkspaceConfig)(root, { languages: readyLanguages });
143
+ // Notify client that tool list may have changed (lsp_init hidden/shown)
144
+ try {
145
+ await this.server.server.sendToolListChanged();
146
+ }
147
+ catch {
148
+ // Client may not support list-changed notifications
149
+ }
150
+ }
75
151
  configureToolHandlers() {
76
152
  this.server.server.registerCapabilities({ tools: {} });
77
153
  this.server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
@@ -79,9 +155,6 @@ class McpServer {
79
155
  }));
80
156
  this.server.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
81
157
  const toolName = request.params.name;
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.`);
84
- }
85
158
  if (toolName !== "lsp_init" && !this.initialized) {
86
159
  return (0, shared_1.noProjectRootResult)();
87
160
  }
@@ -93,7 +166,9 @@ class McpServer {
93
166
  });
94
167
  }
95
168
  listTools() {
96
- return [...this.tools.values()].map((tool) => ({
169
+ return [...this.tools.values()]
170
+ .filter((tool) => !(tool.name === "lsp_init" && this.hideInitTool))
171
+ .map((tool) => ({
97
172
  name: tool.name,
98
173
  description: tool.description,
99
174
  inputSchema: this.toInputSchema(tool.inputSchema),
@@ -0,0 +1,62 @@
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.loadWorkspaceConfig = loadWorkspaceConfig;
7
+ exports.saveWorkspaceConfig = saveWorkspaceConfig;
8
+ exports.loadLastRoot = loadLastRoot;
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ function getConfigDir() {
13
+ const platform = node_os_1.default.platform();
14
+ if (platform === "win32") {
15
+ const appData = process.env["APPDATA"];
16
+ if (appData)
17
+ return node_path_1.default.join(appData, "lsp-mcp");
18
+ return node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Roaming", "lsp-mcp");
19
+ }
20
+ if (platform === "darwin") {
21
+ return node_path_1.default.join(node_os_1.default.homedir(), "Library", "Application Support", "lsp-mcp");
22
+ }
23
+ // Linux / other POSIX — respect XDG
24
+ const xdgConfig = process.env["XDG_CONFIG_HOME"];
25
+ const base = xdgConfig ?? node_path_1.default.join(node_os_1.default.homedir(), ".config");
26
+ return node_path_1.default.join(base, "lsp-mcp");
27
+ }
28
+ const CONFIG_FILE = node_path_1.default.join(getConfigDir(), "config.json");
29
+ async function readConfig() {
30
+ try {
31
+ const raw = await (0, promises_1.readFile)(CONFIG_FILE, "utf8");
32
+ const parsed = JSON.parse(raw);
33
+ if (typeof parsed === "object" &&
34
+ parsed !== null &&
35
+ "workspaces" in parsed &&
36
+ typeof parsed.workspaces === "object") {
37
+ return parsed;
38
+ }
39
+ }
40
+ catch {
41
+ // File missing or malformed — start fresh
42
+ }
43
+ return { workspaces: {} };
44
+ }
45
+ async function writeConfig(config) {
46
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(CONFIG_FILE), { recursive: true });
47
+ await (0, promises_1.writeFile)(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
48
+ }
49
+ async function loadWorkspaceConfig(root) {
50
+ const config = await readConfig();
51
+ return config.workspaces[root] ?? null;
52
+ }
53
+ async function saveWorkspaceConfig(root, entry) {
54
+ const config = await readConfig();
55
+ config.workspaces[root] = entry;
56
+ config.lastRoot = root;
57
+ await writeConfig(config);
58
+ }
59
+ async function loadLastRoot() {
60
+ const config = await readConfig();
61
+ return config.lastRoot ?? null;
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theupsider/lsp-mcp",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Universal LSP MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",