Skip to content

Comments

feat: add streamable-http support#176

Open
radugheo wants to merge 1 commit intomainfrom
feat/support-streamable-http
Open

feat: add streamable-http support#176
radugheo wants to merge 1 commit intomainfrom
feat/support-streamable-http

Conversation

@radugheo
Copy link
Collaborator

@radugheo radugheo commented Feb 20, 2026

Adds support for MCP servers that use the streamable-http transport.

Previously, the runtime only worked with stdio-based servers (one process per session). With this change, a server configured with transport: streamable-http and a url in mcp.json will have its process started once and shared across all sessions, with each session connecting over HTTP via streamable_http_client.

The session layer was refactored into a BaseSessionServer with two implementations:

  • the existing SessionServer for stdio
  • a new StreamableHttpSessionServer for HTTP

The runtime manages the shared process lifecycle: spawning it before registration, polling until it's ready, monitoring it for unexpected exits, and shutting it down.

Copilot AI review requested due to automatic review settings February 20, 2026 08:35
Copy link

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 MCP’s streamable-http transport in the CLI runtime so sessions can connect over HTTP to a shared server process instead of spawning per-session stdio processes.

Changes:

  • Extend McpServer config model with transport (default stdio) and optional url, plus helper is_streamable_http.
  • Refactor session handling into a transport-agnostic base class, with concrete stdio and streamable-http session implementations.
  • Update runtime to spawn/monitor a shared HTTP server process, wait for readiness, and register tools over streamable-http when configured.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/uipath_mcp/_cli/_utils/_config.py Adds transport/url fields and streamable-http detection to server config model.
src/uipath_mcp/_cli/_runtime/_session.py Introduces BaseSessionServer relay logic and adds StreamableHttpSessionServer.
src/uipath_mcp/_cli/_runtime/_runtime.py Implements shared streamable-http server process lifecycle + registration and per-session transport selection.
src/uipath_mcp/_cli/_runtime/_factory.py Adds streamable-http config validation and adjusts error categorization for server-not-found.
Comments suppressed due to low confidence (4)

src/uipath_mcp/_cli/_utils/_config.py:52

  • __repr__ renders url as url='{self.url}', which prints url='None' when unset and can look like an actual string value in logs. Consider omitting url when it is None, or using {self.url!r} without surrounding quotes so the representation is unambiguous.
    def __repr__(self) -> str:
        return f"McpServer(name='{self.name}', type='{self.type}', transport='{self.transport}', command='{self.command}', args={self.args}, url='{self.url}')"

src/uipath_mcp/_cli/_utils/_config.py:47

  • to_dict() now always emits a transport key because transport defaults to 'stdio' and the condition is if self.transport:. If the intent is to preserve the original config shape (only include transport when explicitly configured), consider tracking whether it was provided in the source config and only serializing it in that case.
    def to_dict(self) -> dict[str, Any]:
        """Convert the server model back to a dictionary."""
        result: dict[str, Any] = {
            "type": self.type,
            "command": self.command,
            "args": self.args,
        }
        if self.transport:
            result["transport"] = self.transport
        if self.url:

src/uipath_mcp/_cli/_runtime/_runtime.py:560

  • In the streamable-http registration path, _wait_for_http_server_ready() is called before validating that self._server.url is set, and it can raise a plain ValueError (and after _start_http_server_process() has already spawned a process). Recommend validating url (and raising UiPathMcpRuntimeError with CONFIGURATION_ERROR) before starting the process / waiting for readiness, so misconfiguration fails fast and consistently.
            if self._server.is_streamable_http:
                # spawn process, wait for readiness, connect via HTTP
                await self._start_http_server_process()
                await self._wait_for_http_server_ready()

src/uipath_mcp/_cli/_runtime/_factory.py:131

  • McpServer.command is coerced with str(...), so a missing command becomes the literal string 'None'. This makes validation brittle (requiring server.command == "None" checks). Consider keeping command as str | None in McpServer (no coercion) and validating against None/empty, which will simplify this streamable-http validation and avoid propagating 'None' into subprocess execution.
            if not server.command or server.command == "None":
                raise UiPathMcpRuntimeError(
                    McpErrorCode.CONFIGURATION_ERROR,
                    "Invalid configuration",
                    f"Server '{entrypoint}' uses streamable-http transport but 'command' is not specified in mcp.json",
                    UiPathErrorCategory.USER,
                )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@radugheo radugheo force-pushed the feat/support-streamable-http branch from a5b4bef to 3fd3fa6 Compare February 20, 2026 09:21
@radugheo radugheo force-pushed the feat/support-streamable-http branch from 3fd3fa6 to f9c8cd9 Compare February 20, 2026 15:57
@radugheo radugheo force-pushed the feat/support-streamable-http branch from f9c8cd9 to c1fc90c Compare February 20, 2026 16:00
@radugheo radugheo changed the title 🚧 feat: add streamable-http support feat: add streamable-http support Feb 20, 2026
Copy link

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

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

src/uipath_mcp/_cli/_runtime/_runtime.py:451

  • _http_server_stderr_lines grows without bound as _drain_http_stderr appends every stderr line. For long-running servers this can become a memory leak; consider storing only the last N lines (e.g., collections.deque(maxlen=...)) or truncating when the list exceeds a limit.
            async for line in self._http_server_process.stderr:
                decoded = line.decode("utf-8", errors="replace").rstrip()
                self._http_server_stderr_lines.append(decoded)
                logger.debug(f"HTTP server stderr: {decoded}")

src/uipath_mcp/_cli/_runtime/_session.py:371

  • _run_http_session catches Exception, which includes asyncio.CancelledError in Python 3.11. This will log an error during normal shutdown (when stop() cancels the task) and suppress cancellation semantics. Handle asyncio.CancelledError explicitly (re-raise) and reserve error logging for unexpected failures.
        except Exception as e:
            logger.error(
                f"Unexpected error for HTTP session {self._session_id}: {e}",
                exc_info=True,
            )

src/uipath_mcp/_cli/_utils/_config.py:47

  • McpServer.to_dict() will always include transport because the default "stdio" is truthy. If this dict is used to persist config, it will start writing transport: stdio even when the user didn’t specify it. Consider only including transport when it differs from the default or was explicitly present in the source config.
        if self.transport:
            result["transport"] = self.transport
        if self.url:

src/uipath_mcp/_cli/_utils/_config.py:52

  • McpServer.__repr__ wraps url in quotes unconditionally, so url=None is rendered as url='None', which is misleading in logs/debugging. Consider rendering url via {self.url!r} (or conditionally omitting it) so None is clearly represented.
    def __repr__(self) -> str:
        return f"McpServer(name='{self.name}', type='{self.type}', transport='{self.transport}', command='{self.command}', args={self.args}, url='{self.url}')"

src/uipath_mcp/_cli/_runtime/_runtime.py:551

  • If the shared HTTP server process exits unexpectedly, _monitor_http_server_process currently logs and stops HTTP sessions but leaves the runtime running. Consider also triggering a runtime abort/cancel (or a controlled restart) so the runtime doesn’t remain “up” in a broken state.
            if not self._cancel_event.is_set():
                logger.error(
                    f"HTTP server process exited unexpectedly with code {returncode}"
                )
                # Stop all HTTP sessions, they will fail on next request anyway

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +502 to +505
except httpx.HTTPError:
# Any other HTTP error means server is listening
logger.info("HTTP server is ready (responded with error, but is up)")
return
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

In _wait_for_http_server_ready, treating any httpx.HTTPError as “server is ready” can incorrectly mark readiness on ReadTimeout/WriteTimeout/protocol errors (port open but server not usable). Consider retrying on timeout-related errors and only treating an actual response (any status code) or HTTPStatusError as readiness.

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