@@ -9,7 +9,8 @@ use forge_app::McpClientInfra;
99use forge_domain:: {
1010 Environment , Image , McpHttpServer , McpServerConfig , ToolDefinition , ToolName , ToolOutput ,
1111} ;
12- use http:: { HeaderName , HeaderValue , header} ;
12+ use reqwest:: Client ;
13+ use reqwest:: header:: { HeaderName , HeaderValue } ;
1314use rmcp:: model:: { CallToolRequestParam , ClientInfo , Implementation , InitializeRequestParam } ;
1415use rmcp:: service:: RunningService ;
1516use rmcp:: transport:: sse_client:: SseClientConfig ;
@@ -33,20 +34,57 @@ type RmcpClient = RunningService<RoleClient, InitializeRequestParam>;
3334#[ derive( Clone ) ]
3435pub struct ForgeMcpClient {
3536 client : Arc < RwLock < Option < Arc < RmcpClient > > > > ,
37+ http_client : Arc < Client > ,
3638 config : McpServerConfig ,
3739 env_vars : BTreeMap < String , String > ,
3840 environment : Environment ,
3941 resolved_config : Arc < OnceLock < anyhow:: Result < McpServerConfig > > > ,
4042}
4143
4244impl ForgeMcpClient {
45+ /// Build a reqwest client with default headers from the MCP server config.
46+ fn build_http_client ( http : & McpHttpServer ) -> anyhow:: Result < Client > {
47+ let mut header_map = reqwest:: header:: HeaderMap :: new ( ) ;
48+ for ( key, value) in & http. headers {
49+ if let Ok ( name) = HeaderName :: from_str ( key)
50+ && let Ok ( val) = HeaderValue :: from_str ( value)
51+ {
52+ header_map. insert ( name, val) ;
53+ }
54+ }
55+
56+ Ok ( Client :: builder ( ) . default_headers ( header_map) . build ( ) ?)
57+ }
58+
4359 pub fn new (
4460 config : McpServerConfig ,
4561 env_vars : & BTreeMap < String , String > ,
4662 environment : Environment ,
4763 ) -> Self {
64+ // Try to resolve config early so we can extract headers for the HTTP client.
65+ // If resolution fails, fall back to a plain client (headers will be missing
66+ // but the error will surface when create_connection is called).
67+ let resolved = resolve_http_templates (
68+ match & config {
69+ McpServerConfig :: Http ( http) => http. clone ( ) ,
70+ McpServerConfig :: Stdio ( _) => McpHttpServer {
71+ url : String :: new ( ) ,
72+ headers : BTreeMap :: new ( ) ,
73+ timeout : None ,
74+ disable : false ,
75+ oauth : forge_domain:: McpOAuthSetting :: default ( ) ,
76+ } ,
77+ } ,
78+ env_vars,
79+ ) ;
80+
81+ let http_client = resolved
82+ . and_then ( |http| Self :: build_http_client ( & http) )
83+ . unwrap_or_default ( ) ;
84+
4885 Self {
4986 client : Default :: default ( ) ,
87+ http_client : Arc :: new ( http_client) ,
5088 config,
5189 env_vars : env_vars. clone ( ) ,
5290 environment,
@@ -181,16 +219,16 @@ impl ForgeMcpClient {
181219 http : & McpHttpServer ,
182220 ) -> anyhow:: Result < RmcpClient > {
183221 // Try HTTP first, fall back to SSE if it fails
184- let client = self . reqwest_client ( http ) ? ;
222+ let client = self . reqwest_client ( ) ;
185223 let transport = StreamableHttpClientTransport :: with_client (
186- client. clone ( ) ,
224+ client. as_ref ( ) . clone ( ) ,
187225 StreamableHttpClientTransportConfig :: with_uri ( http. url . clone ( ) ) ,
188226 ) ;
189227 match self . client_info ( ) . serve ( transport) . await {
190228 Ok ( client) => Ok ( client) ,
191229 Err ( _e) => {
192230 let transport = SseClientTransport :: start_with_client (
193- client,
231+ client. as_ref ( ) . clone ( ) ,
194232 SseClientConfig { sse_endpoint : http. url . clone ( ) . into ( ) , ..Default :: default ( ) } ,
195233 )
196234 . await ?;
@@ -358,9 +396,9 @@ impl ForgeMcpClient {
358396 http : & McpHttpServer ,
359397 token : & str ,
360398 ) -> anyhow:: Result < Arc < RmcpClient > > {
361- let client = self . reqwest_client ( http ) ? ;
399+ let client = self . reqwest_client ( ) ;
362400 let transport = StreamableHttpClientTransport :: with_client (
363- client,
401+ client. as_ref ( ) . clone ( ) ,
364402 StreamableHttpClientTransportConfig :: with_uri ( http. url . clone ( ) ) . auth_header ( token) ,
365403 ) ;
366404
@@ -456,14 +494,12 @@ impl ForgeMcpClient {
456494 Ok ( ( code, state) )
457495 }
458496
459- fn reqwest_client ( & self , config : & McpHttpServer ) -> anyhow:: Result < reqwest:: Client > {
460- let mut headers = header:: HeaderMap :: new ( ) ;
461- for ( key, value) in config. headers . iter ( ) {
462- headers. insert ( HeaderName :: from_str ( key) ?, HeaderValue :: from_str ( value) ?) ;
463- }
464-
465- let client = reqwest:: Client :: builder ( ) . default_headers ( headers) ;
466- Ok ( client. build ( ) ?)
497+ fn reqwest_client ( & self ) -> Arc < Client > {
498+ // Reuse the cached HTTP client (with pre-configured default headers)
499+ // to prevent file descriptor leaks. Each reqwest::Client manages its
500+ // own connection pool, so creating new clients for each connection
501+ // leads to "Too many open files" errors.
502+ self . http_client . clone ( )
467503 }
468504
469505 async fn list ( & self ) -> anyhow:: Result < Vec < ToolDefinition > > {
0 commit comments