6060 FETCH_TIMEOUT_SECONDS ,
6161 HOSTED_PATH_GUIDANCE ,
6262 MAX_FETCH_BYTES ,
63+ MAX_HOSTED_BINARY_RESPONSE_BYTES ,
6364 MAX_INLINE_BYTES ,
6465 SERVER_ICON_URL ,
6566 SERVER_VERSION ,
7374 get_appwrite_context ,
7475)
7576from .docs_search import DocsSearch
76- from .error_classification import is_response_parse_error
77+ from .error_classification import HostedBinaryResponseTooLarge , is_response_parse_error
7778from .operator import Operator , _parse_tool_name
7879from .service import Service
7980from .tool_manager import ToolManager
@@ -381,6 +382,11 @@ def register_services(
381382 name ,
382383 allowed_methods = allowed_methods ,
383384 context_scope = context_scope (name ),
385+ binary_response_limit = (
386+ MAX_HOSTED_BINARY_RESPONSE_BYTES
387+ if profile == OAUTH_PROFILE
388+ else None
389+ ),
384390 )
385391 )
386392 return tools_manager
@@ -820,6 +826,126 @@ def _prepare_arguments(tool_info: dict, arguments: dict[str, Any]) -> dict[str,
820826 return prepared_arguments
821827
822828
829+ def _raise_bounded_response_error (response : httpx .Response ) -> None :
830+ """Translate an upstream streaming error into the SDK's public exception."""
831+ body = bytearray ()
832+ for chunk in response .iter_bytes ():
833+ remaining = MAX_INLINE_BYTES - len (body )
834+ if remaining <= 0 :
835+ break
836+ body .extend (chunk [:remaining ])
837+ text = bytes (body ).decode ("utf-8" , errors = "replace" )
838+ message = text or response .reason_phrase
839+ error_type = None
840+ try :
841+ payload = json .loads (text )
842+ if isinstance (payload , dict ):
843+ message = str (payload .get ("message" ) or message )
844+ raw_type = payload .get ("type" )
845+ error_type = str (raw_type ) if raw_type is not None else None
846+ except (TypeError , ValueError ):
847+ pass
848+ raise AppwriteException (message , response .status_code , error_type , text )
849+
850+
851+ def _perform_bounded_binary_client_call (
852+ client : Client ,
853+ tool_name : str ,
854+ method : str ,
855+ path : str = "" ,
856+ headers : dict [str , Any ] | None = None ,
857+ params : dict [str , Any ] | None = None ,
858+ response_type : str = "json" ,
859+ ) -> bytes :
860+ """Stream one SDK binary call into a bounded buffer for hosted HTTP."""
861+ if method .lower () != "get" or response_type != "json" :
862+ raise RuntimeError (f"Unsupported bounded binary request for { tool_name } ." )
863+
864+ request_headers = {
865+ key : value
866+ for key , value in {** client ._global_headers , ** (headers or {})}.items ()
867+ if value
868+ }
869+ # Prevent HTTPX from transparently inflating a compressed response into one
870+ # oversized chunk before the decoded-byte limit can run.
871+ request_headers ["accept-encoding" ] = "identity"
872+ request_params = client .flatten (params or {})
873+ endpoint = client ._endpoint .rstrip ("/" )
874+
875+ with httpx .Client (
876+ verify = not client ._self_signed ,
877+ timeout = FETCH_TIMEOUT_SECONDS ,
878+ follow_redirects = True ,
879+ ) as http_client :
880+ with http_client .stream (
881+ method , endpoint + path , headers = request_headers , params = request_params
882+ ) as response :
883+ # Check before reading success or error bodies: HTTPX decodes
884+ # ``iter_bytes()`` chunks, so either path could otherwise inflate a
885+ # compressed response beyond the limit before we can count it.
886+ content_encoding = response .headers .get ("content-encoding" , "identity" )
887+ if content_encoding .lower ().strip () not in {"" , "identity" }:
888+ raise ValueError (
889+ "Hosted MCP cannot safely return a compressed binary response. "
890+ "Use an Appwrite SDK or REST API for this content."
891+ )
892+
893+ if response .status_code >= 400 :
894+ _raise_bounded_response_error (response )
895+
896+ warning = response .headers .get ("x-appwrite-warning" )
897+ if warning :
898+ for item in warning .split (";" ):
899+ print (f"Warning: { item } " , file = sys .stderr )
900+
901+ declared = response .headers .get ("content-length" )
902+ if declared :
903+ try :
904+ content_length = int (declared )
905+ except ValueError :
906+ content_length = None
907+ if (
908+ content_length is not None
909+ and content_length > MAX_HOSTED_BINARY_RESPONSE_BYTES
910+ ):
911+ raise HostedBinaryResponseTooLarge (
912+ tool_name ,
913+ MAX_HOSTED_BINARY_RESPONSE_BYTES ,
914+ content_length = content_length ,
915+ )
916+
917+ body = bytearray ()
918+ for chunk in response .iter_bytes ():
919+ observed_bytes = len (body ) + len (chunk )
920+ if observed_bytes > MAX_HOSTED_BINARY_RESPONSE_BYTES :
921+ raise HostedBinaryResponseTooLarge (
922+ tool_name ,
923+ MAX_HOSTED_BINARY_RESPONSE_BYTES ,
924+ observed_bytes = observed_bytes ,
925+ )
926+ body .extend (chunk )
927+ return bytes (body )
928+
929+
930+ def _bounded_binary_client_call (
931+ client : Client ,
932+ tool_name : str ,
933+ method : str ,
934+ path : str = "" ,
935+ headers : dict [str , Any ] | None = None ,
936+ params : dict [str , Any ] | None = None ,
937+ response_type : str = "json" ,
938+ ) -> bytes :
939+ try :
940+ return _perform_bounded_binary_client_call (
941+ client , tool_name , method , path , headers , params , response_type
942+ )
943+ except httpx .HTTPError as exc :
944+ # Match the generated SDK contract so callers receive the existing
945+ # Appwrite-formatted tool error instead of an internal HTTPX exception.
946+ raise AppwriteException (str (exc )) from exc
947+
948+
823949def execute_registered_tool (
824950 tools_manager : ToolManager ,
825951 name : str ,
@@ -843,13 +969,39 @@ def execute_registered_tool(
843969 # Re-bind the SDK method to a client authenticated for the current request.
844970 # An explicit client takes precedence (used by tests); otherwise it is resolved
845971 # from the request's OAuth access token.
972+ hosted = client is None
846973 if client is None :
847974 client = resolve_client (target_project , organization_id )
848975 bound_method = getattr (service_cls (client ), method_name )
976+ bounded_binary = (
977+ hosted and inspect .signature (bound_method ).return_annotation is bytes
978+ )
849979
850980 parsed = _parse_tool_name (name )
851981 try :
852- result = bound_method (** prepared_arguments )
982+ if bounded_binary :
983+ original_call = client .call
984+ setattr (
985+ client ,
986+ "call" ,
987+ lambda method , path = "" , headers = None , params = None , response_type = "json" : _bounded_binary_client_call (
988+ client ,
989+ name ,
990+ method ,
991+ path ,
992+ headers ,
993+ params ,
994+ response_type ,
995+ ),
996+ )
997+ try :
998+ result = bound_method (** prepared_arguments )
999+ finally :
1000+ setattr (client , "call" , original_call )
1001+ else :
1002+ result = bound_method (** prepared_arguments )
1003+ except HostedBinaryResponseTooLarge :
1004+ raise
8531005 except AppwriteException as exc :
8541006 error_monitoring .capture_appwrite_exception (
8551007 exc ,
0 commit comments