@@ -2,7 +2,7 @@ import { experimental_createMCPClient, type MCPTransport } from "@ai-sdk/mcp";
22import type { Tool } from "ai" ;
33import { log } from "@/node/services/log" ;
44import { MCPStdioTransport } from "@/node/services/mcpStdioTransport" ;
5- import type { MCPServerMap } from "@/common/types/mcp" ;
5+ import type { MCPServerMap , MCPTestResult } from "@/common/types/mcp" ;
66import type { Runtime } from "@/node/runtime/Runtime" ;
77import type { MCPConfigService } from "@/node/services/mcpConfigService" ;
88import { createRuntime } from "@/node/runtime/runtimeFactory" ;
@@ -120,7 +120,56 @@ function wrapMCPTools(tools: Record<string, Tool>): Record<string, Tool> {
120120 return wrapped ;
121121}
122122
123- export type MCPTestResult = { success : true ; tools : string [ ] } | { success : false ; error : string } ;
123+ export type { MCPTestResult } from "@/common/types/mcp" ;
124+
125+ /**
126+ * Run a test connection to an MCP server command.
127+ * Spawns the process, connects, fetches tools, then closes.
128+ */
129+ async function runServerTest (
130+ command : string ,
131+ projectPath : string ,
132+ logContext : string
133+ ) : Promise < MCPTestResult > {
134+ const runtime = createRuntime ( { type : "local" , srcBaseDir : projectPath } ) ;
135+ const timeoutPromise = new Promise < MCPTestResult > ( ( resolve ) =>
136+ setTimeout ( ( ) => resolve ( { success : false , error : "Connection timed out" } ) , TEST_TIMEOUT_MS )
137+ ) ;
138+
139+ const testPromise = ( async ( ) : Promise < MCPTestResult > => {
140+ let transport : MCPStdioTransport | null = null ;
141+ try {
142+ log . debug ( `[MCP] Testing ${ logContext } ` , { command } ) ;
143+ const execStream = await runtime . exec ( command , {
144+ cwd : projectPath ,
145+ timeout : TEST_TIMEOUT_MS / 1000 ,
146+ } ) ;
147+
148+ transport = new MCPStdioTransport ( execStream ) ;
149+ await transport . start ( ) ;
150+ const client = await experimental_createMCPClient ( { transport } ) ;
151+ const tools = await client . tools ( ) ;
152+ const toolNames = Object . keys ( tools ) ;
153+ await client . close ( ) ;
154+ await transport . close ( ) ;
155+ log . info ( `[MCP] ${ logContext } test successful` , { tools : toolNames } ) ;
156+ return { success : true , tools : toolNames } ;
157+ } catch ( error ) {
158+ const message = error instanceof Error ? error . message : String ( error ) ;
159+ log . warn ( `[MCP] ${ logContext } test failed` , { error : message } ) ;
160+ if ( transport ) {
161+ try {
162+ await transport . close ( ) ;
163+ } catch {
164+ // ignore cleanup errors
165+ }
166+ }
167+ return { success : false , error : message } ;
168+ }
169+ } ) ( ) ;
170+
171+ return Promise . race ( [ testPromise , timeoutPromise ] ) ;
172+ }
124173
125174interface MCPServerInstance {
126175 name : string ;
@@ -155,21 +204,18 @@ export class MCPServerManager {
155204 } ) : Promise < Record < string , Tool > > {
156205 const { workspaceId, projectPath, runtime, workspacePath } = options ;
157206 const servers = await this . configService . listServers ( projectPath ) ;
158- const signature = JSON . stringify ( servers ?? { } ) ;
159- const serverCount = Object . keys ( servers ?? { } ) . length ;
207+ const signature = JSON . stringify ( servers ) ;
208+ const serverNames = Object . keys ( servers ) ;
160209
161210 const existing = this . workspaceServers . get ( workspaceId ) ;
162211 if ( existing ?. configSignature === signature ) {
163- log . debug ( "[MCP] Using cached servers" , { workspaceId, serverCount } ) ;
212+ log . debug ( "[MCP] Using cached servers" , { workspaceId, serverCount : serverNames . length } ) ;
164213 return this . collectTools ( existing . instances ) ;
165214 }
166215
167216 // Config changed or not started yet -> restart
168- if ( serverCount > 0 ) {
169- log . info ( "[MCP] Starting servers" , {
170- workspaceId,
171- servers : Object . keys ( servers ?? { } ) ,
172- } ) ;
217+ if ( serverNames . length > 0 ) {
218+ log . info ( "[MCP] Starting servers" , { workspaceId, servers : serverNames } ) ;
173219 }
174220 await this . stopServers ( workspaceId ) ;
175221 const instances = await this . startServers ( servers , runtime , workspacePath ) ;
@@ -201,49 +247,11 @@ export class MCPServerManager {
201247 */
202248 async testServer ( projectPath : string , name : string ) : Promise < MCPTestResult > {
203249 const servers = await this . configService . listServers ( projectPath ) ;
204- const command = servers ?. [ name ] ;
250+ const command = servers [ name ] ;
205251 if ( ! command ) {
206252 return { success : false , error : `Server "${ name } " not found in configuration` } ;
207253 }
208-
209- const runtime = createRuntime ( { type : "local" , srcBaseDir : projectPath } ) ;
210- const timeoutPromise = new Promise < MCPTestResult > ( ( resolve ) =>
211- setTimeout ( ( ) => resolve ( { success : false , error : "Connection timed out" } ) , TEST_TIMEOUT_MS )
212- ) ;
213-
214- const testPromise = ( async ( ) : Promise < MCPTestResult > => {
215- let transport : MCPStdioTransport | null = null ;
216- try {
217- log . debug ( "[MCP] Testing server" , { name, command } ) ;
218- const execStream = await runtime . exec ( command , {
219- cwd : projectPath ,
220- timeout : TEST_TIMEOUT_MS / 1000 ,
221- } ) ;
222-
223- transport = new MCPStdioTransport ( execStream ) ;
224- await transport . start ( ) ;
225- const client = await experimental_createMCPClient ( { transport } ) ;
226- const tools = await client . tools ( ) ;
227- const toolNames = Object . keys ( tools ) ;
228- await client . close ( ) ;
229- await transport . close ( ) ;
230- log . info ( "[MCP] Test successful" , { name, tools : toolNames } ) ;
231- return { success : true , tools : toolNames } ;
232- } catch ( error ) {
233- const message = error instanceof Error ? error . message : String ( error ) ;
234- log . warn ( "[MCP] Test failed" , { name, error : message } ) ;
235- if ( transport ) {
236- try {
237- await transport . close ( ) ;
238- } catch {
239- // ignore cleanup errors
240- }
241- }
242- return { success : false , error : message } ;
243- }
244- } ) ( ) ;
245-
246- return Promise . race ( [ testPromise , timeoutPromise ] ) ;
254+ return runServerTest ( command , projectPath , `server "${ name } "` ) ;
247255 }
248256
249257 /**
@@ -254,45 +262,7 @@ export class MCPServerManager {
254262 if ( ! command . trim ( ) ) {
255263 return { success : false , error : "Command is required" } ;
256264 }
257-
258- const runtime = createRuntime ( { type : "local" , srcBaseDir : projectPath } ) ;
259- const timeoutPromise = new Promise < MCPTestResult > ( ( resolve ) =>
260- setTimeout ( ( ) => resolve ( { success : false , error : "Connection timed out" } ) , TEST_TIMEOUT_MS )
261- ) ;
262-
263- const testPromise = ( async ( ) : Promise < MCPTestResult > => {
264- let transport : MCPStdioTransport | null = null ;
265- try {
266- log . debug ( "[MCP] Testing command" , { command } ) ;
267- const execStream = await runtime . exec ( command , {
268- cwd : projectPath ,
269- timeout : TEST_TIMEOUT_MS / 1000 ,
270- } ) ;
271-
272- transport = new MCPStdioTransport ( execStream ) ;
273- await transport . start ( ) ;
274- const client = await experimental_createMCPClient ( { transport } ) ;
275- const tools = await client . tools ( ) ;
276- const toolNames = Object . keys ( tools ) ;
277- await client . close ( ) ;
278- await transport . close ( ) ;
279- log . info ( "[MCP] Command test successful" , { command, tools : toolNames } ) ;
280- return { success : true , tools : toolNames } ;
281- } catch ( error ) {
282- const message = error instanceof Error ? error . message : String ( error ) ;
283- log . warn ( "[MCP] Command test failed" , { command, error : message } ) ;
284- if ( transport ) {
285- try {
286- await transport . close ( ) ;
287- } catch {
288- // ignore cleanup errors
289- }
290- }
291- return { success : false , error : message } ;
292- }
293- } ) ( ) ;
294-
295- return Promise . race ( [ testPromise , timeoutPromise ] ) ;
265+ return runServerTest ( command , projectPath , "command" ) ;
296266 }
297267
298268 private collectTools ( instances : Map < string , MCPServerInstance > ) : Record < string , Tool > {
@@ -309,7 +279,7 @@ export class MCPServerManager {
309279 workspacePath : string
310280 ) : Promise < Map < string , MCPServerInstance > > {
311281 const result = new Map < string , MCPServerInstance > ( ) ;
312- const entries = Object . entries ( servers ?? { } ) ;
282+ const entries = Object . entries ( servers ) ;
313283 for ( const [ name , command ] of entries ) {
314284 try {
315285 const instance = await this . startSingleServer ( name , command , runtime , workspacePath ) ;
0 commit comments