66#[ cfg( feature = "composio" ) ]
77use std:: sync:: Arc ;
88
9+ #[ cfg( feature = "composio" ) ]
10+ use std:: sync:: atomic:: { AtomicU32 , AtomicU64 , Ordering } ;
11+
912#[ cfg( feature = "composio" ) ]
1013use async_trait:: async_trait;
1114
@@ -22,15 +25,78 @@ use crate::reasoning::inference::ToolDefinition;
2225#[ cfg( feature = "composio" ) ]
2326use crate :: reasoning:: loop_types:: { LoopConfig , Observation , ProposedAction } ;
2427
28+ /// Simple per-minute rate limiter for MCP tool calls.
29+ #[ cfg( feature = "composio" ) ]
30+ struct McpRateLimiter {
31+ /// Maximum calls allowed per minute (0 = unlimited).
32+ max_per_minute : u32 ,
33+ /// Calls made in the current window.
34+ calls : AtomicU32 ,
35+ /// Start of the current window (seconds since UNIX epoch).
36+ window_start : AtomicU64 ,
37+ }
38+
39+ #[ cfg( feature = "composio" ) ]
40+ impl McpRateLimiter {
41+ fn new ( max_per_minute : Option < u32 > ) -> Self {
42+ let now = std:: time:: SystemTime :: now ( )
43+ . duration_since ( std:: time:: UNIX_EPOCH )
44+ . unwrap_or_default ( )
45+ . as_secs ( ) ;
46+ Self {
47+ max_per_minute : max_per_minute. unwrap_or ( 0 ) ,
48+ calls : AtomicU32 :: new ( 0 ) ,
49+ window_start : AtomicU64 :: new ( now) ,
50+ }
51+ }
52+
53+ /// Check if a call is allowed. Returns `true` if within limits.
54+ fn check ( & self ) -> bool {
55+ if self . max_per_minute == 0 {
56+ return true ; // unlimited
57+ }
58+
59+ let now = std:: time:: SystemTime :: now ( )
60+ . duration_since ( std:: time:: UNIX_EPOCH )
61+ . unwrap_or_default ( )
62+ . as_secs ( ) ;
63+ let window = self . window_start . load ( Ordering :: Relaxed ) ;
64+
65+ // Reset window if more than 60 seconds have passed
66+ if now - window >= 60 {
67+ self . window_start . store ( now, Ordering :: Relaxed ) ;
68+ self . calls . store ( 1 , Ordering :: Relaxed ) ;
69+ return true ;
70+ }
71+
72+ let current = self . calls . fetch_add ( 1 , Ordering :: Relaxed ) ;
73+ current < self . max_per_minute
74+ }
75+ }
76+
2577/// An [`ActionExecutor`] that dispatches tool calls to Composio via JSON-RPC.
2678#[ cfg( feature = "composio" ) ]
2779pub struct ComposioToolExecutor {
2880 transport : Arc < SseTransport > ,
2981 tool_definitions : Vec < ToolDefinition > ,
82+ rate_limiter : McpRateLimiter ,
3083}
3184
3285#[ cfg( feature = "composio" ) ]
3386impl ComposioToolExecutor {
87+ /// Discover available tools from the Composio MCP endpoint and return a
88+ /// new executor ready to dispatch calls.
89+ ///
90+ /// `max_calls_per_minute` enforces a per-server rate limit (None = unlimited).
91+ pub async fn discover_with_rate_limit (
92+ transport : Arc < SseTransport > ,
93+ max_calls_per_minute : Option < u32 > ,
94+ ) -> Result < Self , ComposioError > {
95+ let mut executor = Self :: discover ( transport) . await ?;
96+ executor. rate_limiter = McpRateLimiter :: new ( max_calls_per_minute) ;
97+ Ok ( executor)
98+ }
99+
34100 /// Discover available tools from the Composio MCP endpoint and return a
35101 /// new executor ready to dispatch calls.
36102 pub async fn discover ( transport : Arc < SseTransport > ) -> Result < Self , ComposioError > {
@@ -69,6 +135,7 @@ impl ComposioToolExecutor {
69135 Ok ( Self {
70136 transport,
71137 tool_definitions,
138+ rate_limiter : McpRateLimiter :: new ( None ) ,
72139 } )
73140 }
74141
@@ -79,6 +146,14 @@ impl ComposioToolExecutor {
79146
80147 /// Call a single tool on the Composio MCP endpoint.
81148 async fn call_tool ( & self , name : & str , arguments : & str ) -> Result < String , String > {
149+ if !self . rate_limiter . check ( ) {
150+ tracing:: warn!( tool = name, "MCP rate limit exceeded" ) ;
151+ return Err ( format ! (
152+ "Rate limit exceeded: max {} calls/min for MCP server" ,
153+ self . rate_limiter. max_per_minute
154+ ) ) ;
155+ }
156+
82157 let args: serde_json:: Value =
83158 serde_json:: from_str ( arguments) . unwrap_or ( serde_json:: json!( { } ) ) ;
84159
@@ -199,6 +274,24 @@ mod tests {
199274 assert ! ( defs[ 0 ] . parameters[ "properties" ] [ "text" ] . is_object( ) ) ;
200275 }
201276
277+ #[ test]
278+ fn test_rate_limiter_unlimited ( ) {
279+ let limiter = McpRateLimiter :: new ( None ) ;
280+ for _ in 0 ..1000 {
281+ assert ! ( limiter. check( ) ) ;
282+ }
283+ }
284+
285+ #[ test]
286+ fn test_rate_limiter_enforced ( ) {
287+ let limiter = McpRateLimiter :: new ( Some ( 5 ) ) ;
288+ for _ in 0 ..5 {
289+ assert ! ( limiter. check( ) ) ;
290+ }
291+ // 6th call should be rejected
292+ assert ! ( !limiter. check( ) ) ;
293+ }
294+
202295 #[ test]
203296 fn test_mcp_content_extraction ( ) {
204297 let result = serde_json:: json!( {
0 commit comments