@@ -17,9 +17,8 @@ vi.mock('./mcp-client.js', async () => {
1717 return {
1818 ...originalModule ,
1919 McpClient : vi . fn ( ) ,
20- populateMcpServerCommand : vi . fn ( ( ) => ( {
21- 'test-server' : { } ,
22- } ) ) ,
20+ // Return the input servers unchanged (identity function)
21+ populateMcpServerCommand : vi . fn ( ( servers ) => servers ) ,
2322 } ;
2423} ) ;
2524
@@ -81,4 +80,182 @@ describe('McpClientManager', () => {
8180 expect ( mockedMcpClient . connect ) . not . toHaveBeenCalled ( ) ;
8281 expect ( mockedMcpClient . discover ) . not . toHaveBeenCalled ( ) ;
8382 } ) ;
83+
84+ it ( 'should disconnect all clients when stop is called' , async ( ) => {
85+ // Track disconnect calls across all instances
86+ const disconnectCalls : string [ ] = [ ] ;
87+ vi . mocked ( McpClient ) . mockImplementation (
88+ ( name : string ) =>
89+ ( {
90+ connect : vi . fn ( ) ,
91+ discover : vi . fn ( ) ,
92+ disconnect : vi . fn ( ) . mockImplementation ( ( ) => {
93+ disconnectCalls . push ( name ) ;
94+ return Promise . resolve ( ) ;
95+ } ) ,
96+ getStatus : vi . fn ( ) ,
97+ } ) as unknown as McpClient ,
98+ ) ;
99+ const manager = new McpClientManager (
100+ {
101+ 'test-server' : { } ,
102+ 'another-server' : { } ,
103+ } ,
104+ '' ,
105+ { } as ToolRegistry ,
106+ { } as PromptRegistry ,
107+ false ,
108+ { } as WorkspaceContext ,
109+ ) ;
110+ // First connect to create the clients
111+ await manager . discoverAllMcpTools ( {
112+ isTrustedFolder : ( ) => true ,
113+ } as unknown as Config ) ;
114+
115+ // Clear the disconnect calls from initial stop() in discoverAllMcpTools
116+ disconnectCalls . length = 0 ;
117+
118+ // Then stop
119+ await manager . stop ( ) ;
120+ expect ( disconnectCalls ) . toHaveLength ( 2 ) ;
121+ expect ( disconnectCalls ) . toContain ( 'test-server' ) ;
122+ expect ( disconnectCalls ) . toContain ( 'another-server' ) ;
123+ } ) ;
124+
125+ it ( 'should be idempotent - stop can be called multiple times safely' , async ( ) => {
126+ const mockedMcpClient = {
127+ connect : vi . fn ( ) ,
128+ discover : vi . fn ( ) ,
129+ disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
130+ getStatus : vi . fn ( ) ,
131+ } ;
132+ vi . mocked ( McpClient ) . mockReturnValue (
133+ mockedMcpClient as unknown as McpClient ,
134+ ) ;
135+ const manager = new McpClientManager (
136+ {
137+ 'test-server' : { } ,
138+ } ,
139+ '' ,
140+ { } as ToolRegistry ,
141+ { } as PromptRegistry ,
142+ false ,
143+ { } as WorkspaceContext ,
144+ ) ;
145+ await manager . discoverAllMcpTools ( {
146+ isTrustedFolder : ( ) => true ,
147+ } as unknown as Config ) ;
148+
149+ // Call stop multiple times - should not throw
150+ await manager . stop ( ) ;
151+ await manager . stop ( ) ;
152+ await manager . stop ( ) ;
153+ } ) ;
154+
155+ it ( 'should discover tools for a single server and track the client for stop' , async ( ) => {
156+ const mockedMcpClient = {
157+ connect : vi . fn ( ) ,
158+ discover : vi . fn ( ) ,
159+ disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
160+ getStatus : vi . fn ( ) ,
161+ } ;
162+ vi . mocked ( McpClient ) . mockReturnValue (
163+ mockedMcpClient as unknown as McpClient ,
164+ ) ;
165+
166+ const manager = new McpClientManager (
167+ {
168+ 'test-server' : { } ,
169+ } ,
170+ '' ,
171+ { } as ToolRegistry ,
172+ { } as PromptRegistry ,
173+ false ,
174+ { } as WorkspaceContext ,
175+ ) ;
176+
177+ await manager . discoverMcpToolsForServer (
178+ 'test-server' ,
179+ { } as unknown as Config ,
180+ ) ;
181+
182+ expect ( mockedMcpClient . connect ) . toHaveBeenCalledOnce ( ) ;
183+ expect ( mockedMcpClient . discover ) . toHaveBeenCalledOnce ( ) ;
184+
185+ await manager . stop ( ) ;
186+ expect ( mockedMcpClient . disconnect ) . toHaveBeenCalledOnce ( ) ;
187+ } ) ;
188+
189+ it ( 'should replace an existing client when re-discovering a server' , async ( ) => {
190+ const firstClient = {
191+ connect : vi . fn ( ) ,
192+ discover : vi . fn ( ) ,
193+ disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
194+ getStatus : vi . fn ( ) ,
195+ } ;
196+ const secondClient = {
197+ connect : vi . fn ( ) ,
198+ discover : vi . fn ( ) ,
199+ disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
200+ getStatus : vi . fn ( ) ,
201+ } ;
202+
203+ vi . mocked ( McpClient )
204+ . mockReturnValueOnce ( firstClient as unknown as McpClient )
205+ . mockReturnValueOnce ( secondClient as unknown as McpClient ) ;
206+
207+ const manager = new McpClientManager (
208+ {
209+ 'test-server' : { } ,
210+ } ,
211+ '' ,
212+ { } as ToolRegistry ,
213+ { } as PromptRegistry ,
214+ false ,
215+ { } as WorkspaceContext ,
216+ ) ;
217+
218+ await manager . discoverMcpToolsForServer (
219+ 'test-server' ,
220+ { } as unknown as Config ,
221+ ) ;
222+ await manager . discoverMcpToolsForServer (
223+ 'test-server' ,
224+ { } as unknown as Config ,
225+ ) ;
226+
227+ expect ( firstClient . disconnect ) . toHaveBeenCalledOnce ( ) ;
228+ expect ( secondClient . connect ) . toHaveBeenCalledOnce ( ) ;
229+ expect ( secondClient . discover ) . toHaveBeenCalledOnce ( ) ;
230+
231+ await manager . stop ( ) ;
232+ expect ( secondClient . disconnect ) . toHaveBeenCalledOnce ( ) ;
233+ } ) ;
234+
235+ it ( 'should no-op when discovering an unknown server' , async ( ) => {
236+ const mockedMcpClient = {
237+ connect : vi . fn ( ) ,
238+ discover : vi . fn ( ) ,
239+ disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
240+ getStatus : vi . fn ( ) ,
241+ } ;
242+ vi . mocked ( McpClient ) . mockReturnValue (
243+ mockedMcpClient as unknown as McpClient ,
244+ ) ;
245+
246+ const manager = new McpClientManager (
247+ { } ,
248+ '' ,
249+ { } as ToolRegistry ,
250+ { } as PromptRegistry ,
251+ false ,
252+ { } as WorkspaceContext ,
253+ ) ;
254+
255+ await manager . discoverMcpToolsForServer ( 'unknown-server' , {
256+ isTrustedFolder : ( ) => true ,
257+ } as unknown as Config ) ;
258+
259+ expect ( vi . mocked ( McpClient ) ) . not . toHaveBeenCalled ( ) ;
260+ } ) ;
84261} ) ;
0 commit comments