11"""Forward MCP messages without converting tools, results or notifications."""
22
33from contextlib import asynccontextmanager
4- from functools import partial
54import logging
65import os
6+ import socket
7+ import ssl
78import sys
89
910import anyio
1011import httpx
1112from mcp .client .sse import sse_client
1213from mcp .client .streamable_http import streamable_http_client
1314from mcp .server .stdio import stdio_server
15+ from mcp .shared ._httpx_utils import create_mcp_http_client
1416from mcp .types import JSONRPCRequest
1517
1618
19+ def sandbox_failure_message (error ):
20+ # SDK exception groups and chained HTTP errors can embed credentials. Only
21+ # report numeric HTTP statuses or fixed descriptions, never exception text.
22+ errors , pending , seen = [], [error ], set ()
23+ while pending :
24+ current = pending .pop ()
25+ if id (current ) in seen :
26+ continue
27+ seen .add (id (current ))
28+ errors .append (current )
29+ if isinstance (current , BaseExceptionGroup ):
30+ pending .extend (current .exceptions )
31+ if current .__cause__ is not None :
32+ pending .append (current .__cause__ )
33+ for current in errors :
34+ if isinstance (current , httpx .HTTPStatusError ):
35+ return f"MCP endpoint returned HTTP { current .response .status_code } ; check endpoint and credentials"
36+ for exception_type , message in (
37+ (ssl .SSLCertVerificationError , "MCP TLS certificate verification failed" ),
38+ (socket .gaierror , "MCP hostname resolution failed; check container DNS" ),
39+ (PermissionError , "MCP access denied; check sandbox file and network policy" ),
40+ ((httpx .TimeoutException , TimeoutError ), "MCP connection timed out" ),
41+ (httpx .TooManyRedirects , "MCP endpoint returned too many redirects" ),
42+ (httpx .ConnectError , "MCP connection failed; check container connectivity and sandbox network policy" ),
43+ ):
44+ if any (isinstance (current , exception_type ) for current in errors ):
45+ return message
46+ return "MCP session failed; check endpoint, sandbox setup and network policy"
47+
48+
1749class PipeInput :
1850 """Cancellable pipe reads; a blocked readline thread would delay shutdown."""
1951
@@ -75,11 +107,10 @@ def extract_bootstrap(message):
75107
76108
77109@asynccontextmanager
78- async def remote_transport (bootstrap , http_factory ):
110+ async def remote_transport (bootstrap ):
79111 config = bootstrap ["connection" ]
80112 if config .get ("transport" ) not in ("sse" , "streamable_http" ):
81113 raise ValueError ("Unsupported external MCP transport" )
82- factory = partial (http_factory , url = config ["url" ])
83114 timeout = config .get ("timeout" , 5 if config ["transport" ] == "sse" else 30 )
84115 read_timeout = config .get ("sse_read_timeout" , 300 )
85116 if config ["transport" ] == "sse" :
@@ -88,11 +119,13 @@ async def remote_transport(bootstrap, http_factory):
88119 headers = config .get ("headers" ),
89120 timeout = timeout ,
90121 sse_read_timeout = read_timeout ,
91- httpx_client_factory = factory ,
92122 ) as streams :
93123 yield streams
94124 else :
95- async with factory (headers = config .get ("headers" ), timeout = httpx .Timeout (timeout , read = read_timeout )) as client :
125+ async with create_mcp_http_client (
126+ headers = config .get ("headers" ),
127+ timeout = httpx .Timeout (timeout , read = read_timeout ),
128+ ) as client :
96129 async with streamable_http_client (
97130 config ["url" ],
98131 http_client = client ,
@@ -111,19 +144,19 @@ async def forward(source, destination, cancel_scope):
111144 cancel_scope .cancel ()
112145
113146
114- async def proxy (http_factory ):
147+ async def proxy ():
115148 async with stdio_server (stdin = PipeInput (), stdout = PipeOutput ()) as (local_read , local_write ):
116149 with anyio .fail_after (30 ):
117150 first = await local_read .receive ()
118151 bootstrap = extract_bootstrap (first )
119- async with remote_transport (bootstrap , http_factory ) as (remote_read , remote_write ):
152+ async with remote_transport (bootstrap ) as (remote_read , remote_write ):
120153 async with anyio .create_task_group () as tasks :
121154 tasks .start_soon (forward , remote_read , local_write , tasks .cancel_scope )
122155 await remote_write .send (first )
123156 tasks .start_soon (forward , local_read , remote_write , tasks .cancel_scope )
124157
125158
126- def run (http_factory ):
159+ def run ():
127160 # Remote SDK exceptions may contain authorization headers or URL parameters.
128161 logging .disable (logging .CRITICAL )
129- anyio .run (proxy , http_factory )
162+ anyio .run (proxy )
0 commit comments