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