Skip to content

Add Streamable HTTP protocol support and support for self-signed certs#122

Open
hestela wants to merge 2 commits intoSecretiveShell:masterfrom
hestela:feat/streamable-http
Open

Add Streamable HTTP protocol support and support for self-signed certs#122
hestela wants to merge 2 commits intoSecretiveShell:masterfrom
hestela:feat/streamable-http

Conversation

@hestela
Copy link
Copy Markdown

@hestela hestela commented Apr 4, 2026

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.

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()
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 StreamableHttpClient and wire it into MCPClientManager.
  • Extend HTTP-based server configs (SSE + Streamable HTTP) to support headers and ssl_verify.
  • Update docs and bump the mcp dependency 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.

Comment on lines +20 to +32
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

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
logger.debug(f"pinging session for {self.name}")

await session.send_ping()

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
except asyncio.CancelledError:
self.session = None
raise

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +28
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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
if config.logging.log_server_pings:
logger.debug(f"pinging session for {self.name}")
await session.send_ping()

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
except asyncio.CancelledError:
raise

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +50
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")
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,
)

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +135
### MCP Server Types

MCP-Bridge supports three transport types for MCP servers.

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 11 to 15
"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",
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants