Add Streamable HTTP protocol support and support for self-signed certs#122
Add Streamable HTTP protocol support and support for self-signed certs#122hestela wants to merge 2 commits intoSecretiveShell:masterfrom
Conversation
Adding streamable HTTP protocal allows for homeassistant MCP server to work. Added ability to add custom SSL certs in the case of a self-signed homeassistant SSL certifiacte. See README for examples on how to use both new features.
MCP 1.9.3 API removed incoming_messages in favor of _received_request() and _received_notification()
There was a problem hiding this comment.
Pull request overview
Adds support for connecting to MCP servers via the Streamable HTTP transport (notably for Home Assistant) and introduces configurable TLS verification / custom CA bundles for HTTP-based transports, with accompanying documentation updates.
Changes:
- Add a new
StreamableHttpClientand wire it intoMCPClientManager. - Extend HTTP-based server configs (SSE + Streamable HTTP) to support
headersandssl_verify. - Update docs and bump the
mcpdependency version range.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
README.md |
Documents stdio/SSE/Streamable HTTP server types and self-signed certificate setup. |
pyproject.toml |
Updates mcp dependency constraints to a newer supported range. |
mcp_bridge/mcp_clients/StreamableHttpClient.py |
Implements Streamable HTTP transport client with keepalive pings and TLS verify handling. |
mcp_bridge/mcp_clients/SseClient.py |
Adds headers + TLS verify handling for SSE transport and retains ping loop. |
mcp_bridge/mcp_clients/session.py |
Switches notification handling to _received_notification (aligning with newer session hooks). |
mcp_bridge/mcp_clients/McpClientManager.py |
Registers the new Streamable HTTP client type for config-based construction. |
mcp_bridge/config/final.py |
Adds new server config model(s) and fields (headers, ssl_verify, transport). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ssl_verify = self.config.ssl_verify | ||
|
|
||
| if ssl_verify is not True: | ||
| # sse_client doesn't expose ssl options, so briefly patch httpx.AsyncClient | ||
| # to inject the verify setting before it creates its internal client. | ||
| _original_init = httpx.AsyncClient.__init__ | ||
|
|
||
| def _patched_init(client_self, *args, **kwargs): | ||
| kwargs.setdefault("verify", ssl_verify) | ||
| _original_init(client_self, *args, **kwargs) | ||
|
|
||
| httpx.AsyncClient.__init__ = _patched_init | ||
|
|
There was a problem hiding this comment.
This temporarily monkeypatches httpx.AsyncClient.__init__ globally to inject verify. In an async server with multiple concurrent clients/requests, this can unintentionally change TLS verification settings for unrelated HTTPX clients created during the patch window (including disabling verification), creating both race conditions and potential security exposure. Prefer a solution that doesn’t mutate global HTTPX state (e.g., pass a preconfigured client/transport into the MCP client if supported, or implement a local wrapper around the transport that constructs its own httpx.AsyncClient(verify=...); if patching is unavoidable, at least guard it with a process-wide lock + nesting/reference counting to prevent cross-session interference).
| logger.debug(f"pinging session for {self.name}") | ||
|
|
||
| await session.send_ping() | ||
|
|
There was a problem hiding this comment.
The ping loop catches Exception, which will also catch asyncio.CancelledError and can prevent clean task cancellation/shutdown (it will log an error and then the session maintainer restarts). Re-raise CancelledError (and potentially BaseException subclasses) so shutdown works correctly.
| except asyncio.CancelledError: | |
| self.session = None | |
| raise |
| ssl_verify = self.config.ssl_verify | ||
|
|
||
| if ssl_verify is not True: | ||
| # streamablehttp_client doesn't expose ssl options, so briefly patch | ||
| # httpx.AsyncClient to inject the verify setting. | ||
| _original_init = httpx.AsyncClient.__init__ | ||
| def _patched_init(client_self, *args, **kwargs): | ||
| kwargs.setdefault("verify", ssl_verify) | ||
| _original_init(client_self, *args, **kwargs) | ||
| httpx.AsyncClient.__init__ = _patched_init |
There was a problem hiding this comment.
This uses a global monkeypatch of httpx.AsyncClient.__init__ to force a verify value. That can affect other concurrent HTTPX usage in the process (including requests unrelated to this MCP server) and can lead to race conditions or unintended TLS verification behavior. Prefer an approach that scopes TLS configuration to just this transport (e.g., construct and pass a dedicated httpx.AsyncClient(verify=...) if the SDK supports it, or wrap/replicate the transport’s client creation). If you must patch, add a lock + nesting safety to avoid overlapping patches from multiple sessions.
| if config.logging.log_server_pings: | ||
| logger.debug(f"pinging session for {self.name}") | ||
| await session.send_ping() | ||
|
|
There was a problem hiding this comment.
The broad except Exception in the ping loop will catch asyncio.CancelledError, which can interfere with cancellation on shutdown (and cause the maintainer to restart instead of exiting). Handle CancelledError explicitly by re-raising it before the generic exception handler.
| except asyncio.CancelledError: | |
| raise |
| headers: dict[str, str] = Field(default_factory=dict, description="HTTP headers to send with requests") | ||
| ssl_verify: bool | str = Field(default=True, description="SSL verification: True, False, or path to a CA bundle file") | ||
| transport: Literal["sse"] = Field(default="sse") | ||
|
|
||
|
|
||
| class StreamableHTTPMCPServer(BaseModel): | ||
| url: str = Field(description="URL of the MCP server") | ||
| headers: dict[str, str] = Field(default_factory=dict, description="HTTP headers to send with requests") |
There was a problem hiding this comment.
headers may contain sensitive values (e.g., Authorization: Bearer ...). Because these config models are logged elsewhere via {server_config} (pydantic repr), adding a plain dict[str, str] here risks leaking credentials into logs. Consider using pydantic.SecretStr for values and/or marking the field as repr=False so secrets aren’t printed.
| headers: dict[str, str] = Field(default_factory=dict, description="HTTP headers to send with requests") | |
| ssl_verify: bool | str = Field(default=True, description="SSL verification: True, False, or path to a CA bundle file") | |
| transport: Literal["sse"] = Field(default="sse") | |
| class StreamableHTTPMCPServer(BaseModel): | |
| url: str = Field(description="URL of the MCP server") | |
| headers: dict[str, str] = Field(default_factory=dict, description="HTTP headers to send with requests") | |
| headers: dict[str, str] = Field( | |
| default_factory=dict, | |
| description="HTTP headers to send with requests", | |
| repr=False, | |
| ) | |
| ssl_verify: bool | str = Field(default=True, description="SSL verification: True, False, or path to a CA bundle file") | |
| transport: Literal["sse"] = Field(default="sse") | |
| class StreamableHTTPMCPServer(BaseModel): | |
| url: str = Field(description="URL of the MCP server") | |
| headers: dict[str, str] = Field( | |
| default_factory=dict, | |
| description="HTTP headers to send with requests", | |
| repr=False, | |
| ) |
| ### MCP Server Types | ||
|
|
||
| MCP-Bridge supports three transport types for MCP servers. | ||
|
|
There was a problem hiding this comment.
This section says MCP-Bridge supports “three transport types”, but the code/config also supports Docker-based MCP servers (DockerMCPServer). Either include Docker in this list (and document its config shape) or clarify that this list is only for non-Docker transports, to avoid misleading users.
| "httpx-sse>=0.4.0", | ||
| "lmos-openai-types", | ||
| "loguru>=0.7.3", | ||
| "mcp>=1.2.0,<=1.7.1", | ||
| "mcp>=1.8.0,<=1.9.3", | ||
| "mcpx[docker]>=0.1.1", |
There was a problem hiding this comment.
pyproject.toml updates the allowed mcp version range, but the repo also tracks a uv.lock. To keep installs reproducible (and CI consistent), regenerate and commit the updated lockfile after changing dependency constraints.
Adding streamable HTTP protocal allows for homeassistant MCP server to work. Added ability to add custom SSL certs in the case of a self-signed homeassistant SSL certifiacte.
See README for examples on how to use both new features.