@@ -2285,6 +2285,82 @@ network_policies:
22852285 assert ! ( !message. contains( "purge_cache" ) ) ;
22862286 }
22872287
2288+ #[ test]
2289+ fn mcp_tool_deny_rule_blocks_tools_call ( ) {
2290+ let data = r#"
2291+ network_policies:
2292+ mcp_api:
2293+ name: mcp_api
2294+ endpoints:
2295+ - host: api.example.test
2296+ port: 443
2297+ path: "/mcp"
2298+ protocol: mcp
2299+ enforcement: enforce
2300+ mcp:
2301+ max_body_bytes: 131072
2302+ rules:
2303+ deny:
2304+ - mcp_method: tools/call
2305+ tool: delete_resource
2306+ allow:
2307+ - mcp_method: initialize
2308+ - mcp_method: tools/list
2309+ - mcp_method: tools/call
2310+ tool: read_status
2311+ binaries:
2312+ - { path: /usr/bin/node }
2313+ "# ;
2314+ let engine = OpaEngine :: from_strings ( TEST_POLICY , data) . unwrap ( ) ;
2315+ let tunnel_engine = engine
2316+ . clone_engine_for_tunnel ( engine. current_generation ( ) )
2317+ . unwrap ( ) ;
2318+ let ctx = L7EvalContext {
2319+ host : "api.example.test" . into ( ) ,
2320+ port : 443 ,
2321+ policy_name : "mcp_api" . into ( ) ,
2322+ binary_path : "/usr/bin/node" . into ( ) ,
2323+ ancestors : vec ! [ ] ,
2324+ cmdline_paths : vec ! [ ] ,
2325+ secret_resolver : None ,
2326+ activity_tx : None ,
2327+ dynamic_credentials : None ,
2328+ token_grant_resolver : None ,
2329+ } ;
2330+ let mut request = L7RequestInfo {
2331+ action : "POST" . into ( ) ,
2332+ target : "/mcp" . into ( ) ,
2333+ query_params : std:: collections:: HashMap :: new ( ) ,
2334+ graphql : None ,
2335+ jsonrpc : Some ( crate :: l7:: jsonrpc:: parse_mcp_body (
2336+ br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_status","arguments":{}}}"# ,
2337+ ) ) ,
2338+ } ;
2339+
2340+ let ( allowed, reason) = evaluate_l7_request ( & tunnel_engine, & ctx, & request) . unwrap ( ) ;
2341+ assert ! ( allowed, "{reason}" ) ;
2342+
2343+ request. jsonrpc = Some ( crate :: l7:: jsonrpc:: parse_mcp_body (
2344+ br#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_resource","arguments":{"scope":"workspace/main"}}}"# ,
2345+ ) ) ;
2346+ let parsed = request. jsonrpc . as_ref ( ) . expect ( "parsed MCP request" ) ;
2347+ assert ! (
2348+ parsed. error. is_none( ) ,
2349+ "MCP request should parse: {parsed:?}"
2350+ ) ;
2351+ assert_eq ! (
2352+ parsed. calls. first( ) . and_then( |call| call. tool. as_deref( ) ) ,
2353+ Some ( "delete_resource" )
2354+ ) ;
2355+
2356+ let ( allowed, reason) = evaluate_l7_request ( & tunnel_engine, & ctx, & request) . unwrap ( ) ;
2357+ assert ! ( !allowed, "delete_resource must match the MCP deny rule" ) ;
2358+ assert ! (
2359+ reason. contains( "deny rule" ) ,
2360+ "deny reason should identify policy denial: {reason}"
2361+ ) ;
2362+ }
2363+
22882364 #[ test]
22892365 fn jsonrpc_log_records_digest_not_args ( ) {
22902366 let info = crate :: l7:: jsonrpc:: parse_jsonrpc_body (
0 commit comments