@@ -1153,21 +1153,88 @@ describe("mcp loopback server", () => {
11531153 } ) ;
11541154
11551155 it ( "executes tools for loopback callers" , async ( ) => {
1156- const cronExecute = vi . fn ( async ( ) => ( {
1156+ const cronExecute = vi . fn < MockGatewayTool [ "execute" ] > ( async ( ) => ( {
11571157 content : [ { type : "text" , text : "CRON_EXECUTED" } ] ,
11581158 } ) ) ;
1159+ const args = { action : "status" } ;
11591160 mockScopedTools ( [ makeMessageTool ( ) , makeCronTool ( { execute : cronExecute } ) ] ) ;
11601161 const { runtime } = await startLoopbackServerForTest ( ) ;
11611162
11621163 const payload = await callMainSessionTool ( {
11631164 token : runtime ?. ownerToken ,
11641165 name : "cron" ,
1166+ args,
11651167 } ) ;
11661168
11671169 expect ( cronExecute ) . toHaveBeenCalledTimes ( 1 ) ;
1170+ expect ( getBeforeToolCallHookInput ( 0 ) . params ) . toEqual ( args ) ;
1171+ expect ( cronExecute . mock . calls [ 0 ] ?. [ 1 ] ) . toEqual ( args ) ;
11681172 expectMcpResultText ( payload , "CRON_EXECUTED" ) ;
11691173 } ) ;
11701174
1175+ it . each ( [
1176+ [ "null" , null ] ,
1177+ [ "array" , [ ] ] ,
1178+ [ "string" , "bad" ] ,
1179+ ] ) ( "rejects %s tool call arguments before hooks or execution" , async ( _label , badArguments ) => {
1180+ const execute = vi . fn < MockGatewayTool [ "execute" ] > ( async ( ) => ( {
1181+ content : [ { type : "text" , text : "EXECUTED" } ] ,
1182+ } ) ) ;
1183+ mockScopedTools ( [ makeMessageTool ( { execute } ) ] ) ;
1184+ const { runtime, port } = await startLoopbackServerForTest ( ) ;
1185+
1186+ const response = await sendRaw ( {
1187+ port,
1188+ token : runtime . ownerToken ,
1189+ headers : jsonHeaders ( ) ,
1190+ body : JSON . stringify ( {
1191+ jsonrpc : "2.0" ,
1192+ id : 1 ,
1193+ method : "tools/call" ,
1194+ params : { name : "message" , arguments : badArguments } ,
1195+ } ) ,
1196+ } ) ;
1197+
1198+ expect ( response . status ) . toBe ( 200 ) ;
1199+ expect ( await readMcpPayload ( response ) ) . toEqual ( {
1200+ jsonrpc : "2.0" ,
1201+ id : 1 ,
1202+ error : {
1203+ code : - 32602 ,
1204+ message : "Invalid params: tools/call arguments must be an object" ,
1205+ } ,
1206+ } ) ;
1207+ expect ( runBeforeToolCallHookMock ) . not . toHaveBeenCalled ( ) ;
1208+ expect ( execute ) . not . toHaveBeenCalled ( ) ;
1209+ } ) ;
1210+
1211+ it ( "keeps omitted tool call arguments as an empty object" , async ( ) => {
1212+ const execute = vi . fn < MockGatewayTool [ "execute" ] > ( async ( ) => ( {
1213+ content : [ { type : "text" , text : "EXECUTED" } ] ,
1214+ } ) ) ;
1215+ mockScopedTools ( [ makeMessageTool ( { execute } ) ] ) ;
1216+ const { runtime, port } = await startLoopbackServerForTest ( ) ;
1217+
1218+ const response = await sendRaw ( {
1219+ port,
1220+ token : runtime . ownerToken ,
1221+ headers : jsonHeaders ( ) ,
1222+ body : JSON . stringify ( {
1223+ jsonrpc : "2.0" ,
1224+ id : 1 ,
1225+ method : "tools/call" ,
1226+ params : { name : "message" } ,
1227+ } ) ,
1228+ } ) ;
1229+
1230+ expect ( response . status ) . toBe ( 200 ) ;
1231+ expectMcpResultText ( await readMcpPayload ( response ) , "EXECUTED" ) ;
1232+ expect ( runBeforeToolCallHookMock ) . toHaveBeenCalledTimes ( 1 ) ;
1233+ expect ( getBeforeToolCallHookInput ( 0 ) . params ) . toEqual ( { } ) ;
1234+ expect ( execute ) . toHaveBeenCalledTimes ( 1 ) ;
1235+ expect ( execute . mock . calls [ 0 ] ?. [ 1 ] ) . toEqual ( { } ) ;
1236+ } ) ;
1237+
11711238 it ( "preserves valid MCP content blocks returned by loopback tools" , async ( ) => {
11721239 const content = [
11731240 { type : "text" , text : "caption" , annotations : { audience : [ "user" ] } } ,
0 commit comments