@@ -10,6 +10,14 @@ vi.mock("../../prompts/responses", () => ({
1010 toolResult : vi . fn ( ( result : string ) => `Tool result: ${ result } ` ) ,
1111 toolError : vi . fn ( ( error : string ) => `Tool error: ${ error } ` ) ,
1212 invalidMcpToolArgumentError : vi . fn ( ( server : string , tool : string ) => `Invalid args for ${ server } :${ tool } ` ) ,
13+ unknownMcpToolError : vi . fn ( ( server : string , tool : string , availableTools : string [ ] ) => {
14+ const toolsList = availableTools . length > 0 ? availableTools . join ( ", " ) : "No tools available"
15+ return `Tool '${ tool } ' does not exist on server '${ server } '. Available tools: ${ toolsList } `
16+ } ) ,
17+ unknownMcpServerError : vi . fn ( ( server : string , availableServers : string [ ] ) => {
18+ const list = availableServers . length > 0 ? availableServers . join ( ", " ) : "No servers available"
19+ return `Server '${ server } ' is not configured. Available servers: ${ list } `
20+ } ) ,
1321 } ,
1422} ) )
1523
@@ -18,6 +26,12 @@ vi.mock("../../../i18n", () => ({
1826 if ( key === "mcp:errors.invalidJsonArgument" && params ?. toolName ) {
1927 return `Roo tried to use ${ params . toolName } with an invalid JSON argument. Retrying...`
2028 }
29+ if ( key === "mcp:errors.toolNotFound" && params ) {
30+ return `Tool '${ params . toolName } ' does not exist on server '${ params . serverName } '. Available tools: ${ params . availableTools } `
31+ }
32+ if ( key === "mcp:errors.serverNotFound" && params ) {
33+ return `MCP server '${ params . serverName } ' is not configured. Available servers: ${ params . availableServers } `
34+ }
2135 return key
2236 } ) ,
2337} ) )
@@ -40,6 +54,7 @@ describe("useMcpToolTool", () => {
4054 deref : vi . fn ( ) . mockReturnValue ( {
4155 getMcpHub : vi . fn ( ) . mockReturnValue ( {
4256 callTool : vi . fn ( ) ,
57+ getAllServers : vi . fn ( ) . mockReturnValue ( [ ] ) ,
4358 } ) ,
4459 postMessageToWebview : vi . fn ( ) ,
4560 } ) ,
@@ -224,6 +239,10 @@ describe("useMcpToolTool", () => {
224239 partial : false ,
225240 }
226241
242+ // Ensure validation does not fail due to unknown server by returning no provider once
243+ // This makes validateToolExists return isValid: true and proceed to askApproval
244+ mockProviderRef . deref . mockReturnValueOnce ( undefined as any )
245+
227246 mockAskApproval . mockResolvedValue ( false )
228247
229248 await useMcpToolTool (
@@ -252,6 +271,19 @@ describe("useMcpToolTool", () => {
252271 partial : false ,
253272 }
254273
274+ // Ensure validation passes so askApproval is reached and throws
275+ mockProviderRef . deref . mockReturnValueOnce ( {
276+ getMcpHub : ( ) => ( {
277+ getAllServers : vi
278+ . fn ( )
279+ . mockReturnValue ( [
280+ { name : "test_server" , tools : [ { name : "test_tool" , description : "desc" } ] } ,
281+ ] ) ,
282+ callTool : vi . fn ( ) ,
283+ } ) ,
284+ postMessageToWebview : vi . fn ( ) ,
285+ } )
286+
255287 const error = new Error ( "Unexpected error" )
256288 mockAskApproval . mockRejectedValue ( error )
257289
@@ -266,5 +298,239 @@ describe("useMcpToolTool", () => {
266298
267299 expect ( mockHandleError ) . toHaveBeenCalledWith ( "executing MCP tool" , error )
268300 } )
301+
302+ it ( "should reject unknown tool names" , async ( ) => {
303+ // Reset consecutiveMistakeCount for this test
304+ mockTask . consecutiveMistakeCount = 0
305+
306+ const mockServers = [
307+ {
308+ name : "test-server" ,
309+ tools : [
310+ { name : "existing-tool-1" , description : "Tool 1" } ,
311+ { name : "existing-tool-2" , description : "Tool 2" } ,
312+ ] ,
313+ } ,
314+ ]
315+
316+ mockProviderRef . deref . mockReturnValue ( {
317+ getMcpHub : ( ) => ( {
318+ getAllServers : vi . fn ( ) . mockReturnValue ( mockServers ) ,
319+ callTool : vi . fn ( ) ,
320+ } ) ,
321+ postMessageToWebview : vi . fn ( ) ,
322+ } )
323+
324+ const block : ToolUse = {
325+ type : "tool_use" ,
326+ name : "use_mcp_tool" ,
327+ params : {
328+ server_name : "test-server" ,
329+ tool_name : "non-existing-tool" ,
330+ arguments : JSON . stringify ( { test : "data" } ) ,
331+ } ,
332+ partial : false ,
333+ }
334+
335+ await useMcpToolTool (
336+ mockTask as Task ,
337+ block ,
338+ mockAskApproval ,
339+ mockHandleError ,
340+ mockPushToolResult ,
341+ mockRemoveClosingTag ,
342+ )
343+
344+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
345+ expect ( mockTask . recordToolError ) . toHaveBeenCalledWith ( "use_mcp_tool" )
346+ expect ( mockTask . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "does not exist" ) )
347+ // Check that the error message contains available tools
348+ expect ( mockPushToolResult ) . toHaveBeenCalledWith ( expect . stringContaining ( "existing-tool-1" ) )
349+ expect ( mockPushToolResult ) . toHaveBeenCalledWith ( expect . stringContaining ( "existing-tool-2" ) )
350+ } )
351+
352+ it ( "should handle server with no tools" , async ( ) => {
353+ // Reset consecutiveMistakeCount for this test
354+ mockTask . consecutiveMistakeCount = 0
355+
356+ const mockServers = [
357+ {
358+ name : "test-server" ,
359+ tools : [ ] ,
360+ } ,
361+ ]
362+
363+ mockProviderRef . deref . mockReturnValue ( {
364+ getMcpHub : ( ) => ( {
365+ getAllServers : vi . fn ( ) . mockReturnValue ( mockServers ) ,
366+ callTool : vi . fn ( ) ,
367+ } ) ,
368+ postMessageToWebview : vi . fn ( ) ,
369+ } )
370+
371+ const block : ToolUse = {
372+ type : "tool_use" ,
373+ name : "use_mcp_tool" ,
374+ params : {
375+ server_name : "test-server" ,
376+ tool_name : "any-tool" ,
377+ arguments : JSON . stringify ( { test : "data" } ) ,
378+ } ,
379+ partial : false ,
380+ }
381+
382+ await useMcpToolTool (
383+ mockTask as Task ,
384+ block ,
385+ mockAskApproval ,
386+ mockHandleError ,
387+ mockPushToolResult ,
388+ mockRemoveClosingTag ,
389+ )
390+
391+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
392+ expect ( mockTask . recordToolError ) . toHaveBeenCalledWith ( "use_mcp_tool" )
393+ expect ( mockTask . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "does not exist" ) )
394+ expect ( mockPushToolResult ) . toHaveBeenCalledWith ( expect . stringContaining ( "No tools available" ) )
395+ } )
396+
397+ it ( "should allow valid tool names" , async ( ) => {
398+ // Reset consecutiveMistakeCount for this test
399+ mockTask . consecutiveMistakeCount = 0
400+
401+ const mockServers = [
402+ {
403+ name : "test-server" ,
404+ tools : [ { name : "valid-tool" , description : "Valid Tool" } ] ,
405+ } ,
406+ ]
407+
408+ const mockToolResult = {
409+ content : [ { type : "text" , text : "Tool executed successfully" } ] ,
410+ }
411+
412+ mockProviderRef . deref . mockReturnValue ( {
413+ getMcpHub : ( ) => ( {
414+ getAllServers : vi . fn ( ) . mockReturnValue ( mockServers ) ,
415+ callTool : vi . fn ( ) . mockResolvedValue ( mockToolResult ) ,
416+ } ) ,
417+ postMessageToWebview : vi . fn ( ) ,
418+ } )
419+
420+ const block : ToolUse = {
421+ type : "tool_use" ,
422+ name : "use_mcp_tool" ,
423+ params : {
424+ server_name : "test-server" ,
425+ tool_name : "valid-tool" ,
426+ arguments : JSON . stringify ( { test : "data" } ) ,
427+ } ,
428+ partial : false ,
429+ }
430+
431+ mockAskApproval . mockResolvedValue ( true )
432+
433+ await useMcpToolTool (
434+ mockTask as Task ,
435+ block ,
436+ mockAskApproval ,
437+ mockHandleError ,
438+ mockPushToolResult ,
439+ mockRemoveClosingTag ,
440+ )
441+
442+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 0 )
443+ expect ( mockTask . recordToolError ) . not . toHaveBeenCalled ( )
444+ expect ( mockTask . say ) . toHaveBeenCalledWith ( "mcp_server_request_started" )
445+ expect ( mockTask . say ) . toHaveBeenCalledWith ( "mcp_server_response" , "Tool executed successfully" )
446+ } )
447+
448+ it ( "should reject unknown server names with available servers listed" , async ( ) => {
449+ // Arrange
450+ mockTask . consecutiveMistakeCount = 0
451+
452+ const mockServers = [ { name : "s1" , tools : [ ] } ]
453+ const callToolMock = vi . fn ( )
454+
455+ mockProviderRef . deref . mockReturnValue ( {
456+ getMcpHub : ( ) => ( {
457+ getAllServers : vi . fn ( ) . mockReturnValue ( mockServers ) ,
458+ callTool : callToolMock ,
459+ } ) ,
460+ postMessageToWebview : vi . fn ( ) ,
461+ } )
462+
463+ const block : ToolUse = {
464+ type : "tool_use" ,
465+ name : "use_mcp_tool" ,
466+ params : {
467+ server_name : "unknown" ,
468+ tool_name : "any-tool" ,
469+ arguments : "{}" ,
470+ } ,
471+ partial : false ,
472+ }
473+
474+ // Act
475+ await useMcpToolTool (
476+ mockTask as Task ,
477+ block ,
478+ mockAskApproval ,
479+ mockHandleError ,
480+ mockPushToolResult ,
481+ mockRemoveClosingTag ,
482+ )
483+
484+ // Assert
485+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
486+ expect ( mockTask . recordToolError ) . toHaveBeenCalledWith ( "use_mcp_tool" )
487+ expect ( mockTask . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "not configured" ) )
488+ expect ( mockPushToolResult ) . toHaveBeenCalledWith ( expect . stringContaining ( "s1" ) )
489+ expect ( callToolMock ) . not . toHaveBeenCalled ( )
490+ expect ( mockAskApproval ) . not . toHaveBeenCalled ( )
491+ } )
492+
493+ it ( "should reject unknown server names when no servers are available" , async ( ) => {
494+ // Arrange
495+ mockTask . consecutiveMistakeCount = 0
496+
497+ const callToolMock = vi . fn ( )
498+ mockProviderRef . deref . mockReturnValue ( {
499+ getMcpHub : ( ) => ( {
500+ getAllServers : vi . fn ( ) . mockReturnValue ( [ ] ) ,
501+ callTool : callToolMock ,
502+ } ) ,
503+ postMessageToWebview : vi . fn ( ) ,
504+ } )
505+
506+ const block : ToolUse = {
507+ type : "tool_use" ,
508+ name : "use_mcp_tool" ,
509+ params : {
510+ server_name : "unknown" ,
511+ tool_name : "any-tool" ,
512+ arguments : "{}" ,
513+ } ,
514+ partial : false ,
515+ }
516+
517+ // Act
518+ await useMcpToolTool (
519+ mockTask as Task ,
520+ block ,
521+ mockAskApproval ,
522+ mockHandleError ,
523+ mockPushToolResult ,
524+ mockRemoveClosingTag ,
525+ )
526+
527+ // Assert
528+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
529+ expect ( mockTask . recordToolError ) . toHaveBeenCalledWith ( "use_mcp_tool" )
530+ expect ( mockTask . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "not configured" ) )
531+ expect ( mockPushToolResult ) . toHaveBeenCalledWith ( expect . stringContaining ( "No servers available" ) )
532+ expect ( callToolMock ) . not . toHaveBeenCalled ( )
533+ expect ( mockAskApproval ) . not . toHaveBeenCalled ( )
534+ } )
269535 } )
270536} )
0 commit comments