@@ -239,8 +239,8 @@ impl ToolCladExecutor {
239239
240240 // Interpolate URL with args and secrets
241241 let url = interpolate ( & http. url , validated) ;
242- let url =
243- super :: template_vars :: inject_secrets ( & url ) . map_err ( |e| format ! ( "URL secret error: {}" , e) ) ?;
242+ let url = super :: template_vars :: inject_secrets ( & url )
243+ . map_err ( |e| format ! ( "URL secret error: {}" , e) ) ?;
244244
245245 // Interpolate headers with secrets
246246 let mut headers = Vec :: new ( ) ;
@@ -1215,4 +1215,185 @@ type = "object"
12151215 "2.5.0"
12161216 ) ;
12171217 }
1218+
1219+ // ---- MCP Proxy Tests ----
1220+
1221+ #[ test]
1222+ fn test_mcp_proxy_tool_def_generation ( ) {
1223+ let manifest: Manifest = toml:: from_str (
1224+ r#"
1225+ [tool]
1226+ name = "governed_search"
1227+ version = "1.0.0"
1228+ description = "Search via governed MCP proxy"
1229+
1230+ [args.query]
1231+ position = 1
1232+ required = true
1233+ type = "string"
1234+ description = "Search query"
1235+
1236+ [args.max_results]
1237+ position = 2
1238+ required = false
1239+ type = "integer"
1240+ description = "Maximum results to return"
1241+ default = 10
1242+
1243+ [mcp]
1244+ server = "brave-search"
1245+ tool = "brave_web_search"
1246+
1247+ [mcp.field_map]
1248+ query = "q"
1249+ max_results = "count"
1250+
1251+ [output]
1252+ format = "json"
1253+
1254+ [output.schema]
1255+ type = "object"
1256+ "# ,
1257+ )
1258+ . unwrap ( ) ;
1259+ let td = generate_oneshot_tool_def ( & manifest) ;
1260+ assert_eq ! ( td. name, "governed_search" ) ;
1261+ assert_eq ! ( td. description, "Search via governed MCP proxy" ) ;
1262+ let props = td. parameters [ "properties" ] . as_object ( ) . unwrap ( ) ;
1263+ assert ! ( props. contains_key( "query" ) ) ;
1264+ assert ! ( props. contains_key( "max_results" ) ) ;
1265+ let required = td. parameters [ "required" ] . as_array ( ) . unwrap ( ) ;
1266+ assert ! ( required. contains( & serde_json:: json!( "query" ) ) ) ;
1267+ }
1268+
1269+ #[ test]
1270+ fn test_mcp_proxy_execution_returns_delegated_envelope ( ) {
1271+ let manifest: Manifest = toml:: from_str (
1272+ r#"
1273+ [tool]
1274+ name = "governed_search"
1275+ version = "1.0.0"
1276+ description = "Search via governed MCP proxy"
1277+
1278+ [args.query]
1279+ position = 1
1280+ required = true
1281+ type = "string"
1282+ description = "Search query"
1283+
1284+ [mcp]
1285+ server = "brave-search"
1286+ tool = "brave_web_search"
1287+
1288+ [mcp.field_map]
1289+ query = "q"
1290+
1291+ [output]
1292+ format = "json"
1293+
1294+ [output.schema]
1295+ type = "object"
1296+ "# ,
1297+ )
1298+ . unwrap ( ) ;
1299+
1300+ let executor =
1301+ ToolCladExecutor :: new ( vec ! [ ( "governed_search" . to_string( ) , manifest. clone( ) ) ] ) ;
1302+
1303+ let mut args = HashMap :: new ( ) ;
1304+ args. insert ( "query" . to_string ( ) , "rust async" . to_string ( ) ) ;
1305+ let result = executor
1306+ . execute_mcp_backend ( "governed_search" , & manifest, & args)
1307+ . unwrap ( ) ;
1308+
1309+ assert_eq ! ( result[ "status" ] , "delegated" ) ;
1310+ assert_eq ! ( result[ "tool" ] , "governed_search" ) ;
1311+ assert_eq ! ( result[ "mcp_server" ] , "brave-search" ) ;
1312+ assert_eq ! ( result[ "mcp_tool" ] , "brave_web_search" ) ;
1313+ assert_eq ! ( result[ "exit_code" ] , 0 ) ;
1314+
1315+ // Check that field mapping was applied
1316+ let mcp_args = & result[ "mcp_arguments" ] ;
1317+ assert_eq ! ( mcp_args[ "q" ] , "rust async" ) ;
1318+ }
1319+
1320+ #[ test]
1321+ fn test_mcp_proxy_field_map_passthrough ( ) {
1322+ let manifest: Manifest = toml:: from_str (
1323+ r#"
1324+ [tool]
1325+ name = "passthrough"
1326+ version = "1.0.0"
1327+ description = "Direct passthrough"
1328+
1329+ [args.input]
1330+ position = 1
1331+ required = true
1332+ type = "string"
1333+ description = "Input value"
1334+
1335+ [mcp]
1336+ server = "my-server"
1337+ tool = "upstream_tool"
1338+
1339+ [output]
1340+ format = "json"
1341+
1342+ [output.schema]
1343+ type = "object"
1344+ "# ,
1345+ )
1346+ . unwrap ( ) ;
1347+
1348+ let executor = ToolCladExecutor :: new ( vec ! [ ( "passthrough" . to_string( ) , manifest. clone( ) ) ] ) ;
1349+
1350+ let mut args = HashMap :: new ( ) ;
1351+ args. insert ( "input" . to_string ( ) , "hello" . to_string ( ) ) ;
1352+ let result = executor
1353+ . execute_mcp_backend ( "passthrough" , & manifest, & args)
1354+ . unwrap ( ) ;
1355+
1356+ // No field_map, so "input" stays as "input" in upstream args
1357+ let mcp_args = & result[ "mcp_arguments" ] ;
1358+ assert_eq ! ( mcp_args[ "input" ] , "hello" ) ;
1359+ }
1360+
1361+ #[ test]
1362+ fn test_mcp_proxy_dispatch_via_execute_tool ( ) {
1363+ let manifest: Manifest = toml:: from_str (
1364+ r#"
1365+ [tool]
1366+ name = "mcp_tool"
1367+ version = "1.0.0"
1368+ description = "MCP proxy tool"
1369+
1370+ [args.query]
1371+ position = 1
1372+ required = true
1373+ type = "string"
1374+ description = "Query"
1375+
1376+ [mcp]
1377+ server = "test-server"
1378+ tool = "test_tool"
1379+
1380+ [output]
1381+ format = "json"
1382+
1383+ [output.schema]
1384+ type = "object"
1385+ "# ,
1386+ )
1387+ . unwrap ( ) ;
1388+
1389+ let executor = ToolCladExecutor :: new ( vec ! [ ( "mcp_tool" . to_string( ) , manifest) ] ) ;
1390+
1391+ let result = executor
1392+ . execute_tool ( "mcp_tool" , r#"{"query": "test"}"# )
1393+ . unwrap ( ) ;
1394+
1395+ assert_eq ! ( result[ "status" ] , "delegated" ) ;
1396+ assert_eq ! ( result[ "mcp_server" ] , "test-server" ) ;
1397+ assert_eq ! ( result[ "mcp_tool" ] , "test_tool" ) ;
1398+ }
12181399}
0 commit comments