Skip to content

feat(openapi_tool): expose httpx_client_factory on RestApiTool / OpenAPIToolset (sister-API gap with MCP toolset) #5681

@darshil3011

Description

@darshil3011

** Please make sure you read the contribution guide and file the issues in the right place. **
Contribution guide.

🔴 Required Information

Is your feature request related to a specific problem?

I need to control the httpx.AsyncClient used by RestApiTool (OpenAPI-generated tools) — for things like custom CA bundles for
mTLS, corporate proxies, request signing transports, HTTP/2, or observability instrumentation. Today RestApiTool exposes
ssl_verify and a header_provider, but the underlying client is built inside the module-level helper _request(...) in
src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py with only verify=... and timeout=None — none of the
other httpx.AsyncClient knobs are reachable.

The sibling primitive — MCPSessionManager — already solved this in PR #2997 (issue #3005) by accepting an httpx_client_factory
parameter. In the body of that PR the author explicitly wrote:

"exposing a httpx factory is a good pattern which could be followed for those issues too" — referring to similar requests on
other ADK surfaces.

This issue requests the same treatment for RestApiTool (and its toolset).

Describe the Solution You'd Like

Add an optional httpx_client_factory parameter to:

  1. RestApiTool.__init__ (src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py:93-108)
  2. RestApiTool.from_parsed_operation (same file)
  3. OpenAPIToolset.__init__ (src/google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py:65) — plumbed down to
    every generated RestApiTool exactly the way ssl_verify and header_provider already are (see lines 238-239 of
    openapi_toolset.py).

The factory follows the same protocol used by MCPSessionManager — a zero-argument (or headers/timeout/auth-aware) callable
returning an httpx.AsyncClient. When set, the request path uses the user-supplied client instead of constructing a fresh one.

Defaults are unchanged: if httpx_client_factory is None, behaviour is exactly today's (an httpx.AsyncClient(verify=..., timeout=None)).

Impact on your work

Without this, the only escape hatches are:

  • ssl_verify=ssl.SSLContext(...) — works for one specific case (custom CAs) but doesn't help with proxies, HTTP/2, custom
    transports, request signing, or shared connection pooling.
  • Sub-classing RestApiTool and overriding the call to the module-level _request(...) — but _request is a private function,
    not a method, so the user has to monkeypatch or fork.

Both are brittle compared with the clean httpx_client_factory=... pattern already merged for MCP.

Willingness to contribute

Yes — I'd like to submit a PR for this. Planned scope:

  • Add httpx_client_factory: Optional[Callable[..., httpx.AsyncClient]] = None to RestApiTool.__init__,
    RestApiTool.from_parsed_operation, and OpenAPIToolset.__init__.
  • Plumb it through OpenAPIToolset._generate_tools_from_operations (where ssl_verify and header_provider are already
    forwarded).
  • Modify _request(...) to use the factory when provided, falling back to today's httpx.AsyncClient(verify=..., timeout=None)
    when not.
  • Add unit tests under tests/unittests/tools/openapi_tool/openapi_spec_parser/ mirroring the test added in PR Feat/expose mcps streamable http custom httpx factory parameter #2997 — assert the
    custom factory is invoked and that the default path remains unchanged.

🟡 Recommended Information

Describe Alternatives You've Considered

  • ssl_verify only — covers the custom-CA case but not proxies, HTTP/2, custom transports, request signing, or shared pools.
  • Sub-classing RestApiTool_request is a private module function, not a method, so the only override path is
    monkeypatching the module, which is brittle and breaks on every refactor.
  • Wrapping the OpenAPI spec in a proxy — moves the problem one layer up; doesn't help with credentials or per-request transport
    policy.

Proposed API / Implementation

Following the MCP precedent (StreamableHTTPConnectionParams.httpx_client_factory):

from typing import Callable, Optional
import httpx

HttpxClientFactory = Callable[..., httpx.AsyncClient]

class RestApiTool(BaseTool):
    def __init__(
        self,
        name: str,
        description: str,
        endpoint: Union[OperationEndpoint, str],
        operation: Union[Operation, str],
        auth_scheme: Optional[Union[AuthScheme, str]] = None,
        auth_credential: Optional[Union[AuthCredential, str]] = None,
        should_parse_operation: bool = True,
        ssl_verify: Optional[Union[bool, str, ssl.SSLContext]] = None,
        header_provider: Optional[
            Callable[[ReadonlyContext], Dict[str, str]]
        ] = None,
        httpx_client_factory: Optional[HttpxClientFactory] = None,  # NEW
        *,
        credential_key: Optional[str] = None,
    ):
        ...
        self._httpx_client_factory = httpx_client_factory


class OpenAPIToolset(BaseToolset):
    def __init__(
        self,
        ...,
        ssl_verify: Optional[Union[bool, str, ssl.SSLContext]] = None,
        header_provider: Optional[Callable[[ReadonlyContext], Dict[str, str]]] = None,
        httpx_client_factory: Optional[HttpxClientFactory] = None,  # NEW
    ):
        ...
        self._httpx_client_factory = httpx_client_factory

Usage:

import httpx
from google.adk.tools.openapi_tool import OpenAPIToolset

def _client_factory(**kwargs) -> httpx.AsyncClient:
    return httpx.AsyncClient(
        verify="/etc/ssl/corp-ca.pem",
        proxies="http://corp-proxy:3128",
        http2=True,
        timeout=httpx.Timeout(60.0),
        **kwargs,
    )

toolset = OpenAPIToolset(
    spec_str=open("petstore.yaml").read(),
    spec_str_type="yaml",
    httpx_client_factory=_client_factory,
)

Internally, _request(...) would become something like:

async def _request(
    *,
    httpx_client_factory: Optional[HttpxClientFactory] = None,
    **request_params,
) -> httpx.Response:
    if httpx_client_factory is not None:
        async with httpx_client_factory() as client:
            return await client.request(**request_params)
    async with httpx.AsyncClient(
        verify=request_params.pop("verify", True),
        timeout=None,
    ) as client:
        return await client.request(**request_params)

Additional Context

Versions checked: google-adk 1.33.0 / main. Files:

  • src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py
  • src/google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py

Metadata

Metadata

Assignees

Labels

needs review[Status] The PR/issue is awaiting review from the maintainertools[Component] This issue is related to tools

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions