From f31cd3e73c02264822e66af30feaf1bac66448b2 Mon Sep 17 00:00:00 2001 From: Sergey Parfenyuk Date: Mon, 26 May 2025 09:50:35 +0200 Subject: [PATCH] feat: support streamable transport in client mode (#70) When running in client mode, use `--transport=streamablehttp` to enforce the new communication method between `mcp-proxy` and remote MCP servers. --- README.md | 62 ++++++++++++++------------ pyproject.toml | 2 +- src/mcp_proxy/__main__.py | 23 +++++++--- src/mcp_proxy/streamablehttp_client.py | 30 +++++++++++++ uv.lock | 8 ++-- 5 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 src/mcp_proxy/streamablehttp_client.py diff --git a/README.md b/README.md index c3ef72c..69c9f00 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,23 @@ [![smithery badge](https://smithery.ai/badge/mcp-proxy)](https://smithery.ai/server/mcp-proxy) - [mcp-proxy](#mcp-proxy) - - [About](#about) - - [1. stdio to SSE/StreamableHttp](#1-stdio-to-sse) - - [1.1 Configuration](#11-configuration) - - [1.2 Example usage](#12-example-usage) - - [2. SSE to stdio](#2-sse-to-stdio) - - [2.1 Configuration](#21-configuration) - - [2.2 Example usage](#22-example-usage) - - [Installation](#installation) - - [Installing via Smithery](#installing-via-smithery) - - [Installing via PyPI](#installing-via-pypi) - - [Installing via Github repository (latest)](#installing-via-github-repository-latest) - - [Installing as container](#installing-as-container) - - [Troubleshooting](#troubleshooting) - - [Extending the container image](#extending-the-container-image) - - [Docker Compose Setup](#docker-compose-setup) - - [Command line arguments](#command-line-arguments) - - [Testing](#testing) + - [About](#about) + - [1. stdio to SSE/StreamableHTTP](#1-stdio-to-ssestreamablehttp) + - [1.1 Configuration](#11-configuration) + - [1.2 Example usage](#12-example-usage) + - [2. SSE to stdio](#2-sse-to-stdio) + - [2.1 Configuration](#21-configuration) + - [2.2 Example usage](#22-example-usage) + - [Installation](#installation) + - [Installing via Smithery](#installing-via-smithery) + - [Installing via PyPI](#installing-via-pypi) + - [Installing via Github repository (latest)](#installing-via-github-repository-latest) + - [Installing as container](#installing-as-container) + - [Troubleshooting](#troubleshooting) + - [Extending the container image](#extending-the-container-image) + - [Docker Compose Setup](#docker-compose-setup) + - [Command line arguments](#command-line-arguments) + - [Testing](#testing) ## About @@ -51,19 +51,20 @@ graph LR ### 1.1 Configuration -This mode requires passing the URL to the MCP Server SSE endpoint as the first argument to the program. +This mode requires providing the URL of the MCP Server's SSE endpoint as the program’s first argument. If the server uses Streamable HTTP transport, make sure to enforce it on the `mcp-proxy` side by passing `--transport=streamablehttp`. Arguments -| Name | Required | Description | Example | -|------------------|----------|--------------------------------------------------|-----------------------------------------------| -| `command_or_url` | Yes | The MCP server SSE endpoint to connect to | http://example.io/sse | -| `--headers` | No | Headers to use for the MCP server SSE connection | Authorization 'Bearer my-secret-access-token' | +| Name | Required | Description | Example | +| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| `command_or_url` | Yes | The MCP server SSE endpoint to connect to | http://example.io/sse | +| `--headers` | No | Headers to use for the MCP server SSE connection | Authorization 'Bearer my-secret-access-token' | +| `--transport` | No | Decides which transport protocol to use when connecting to an MCP server. Can be either 'sse' or 'streamablehttp' | streamablehttp | Environment Variables | Name | Required | Description | Example | -|--------------------|----------|------------------------------------------------------------------------------|------------| +| ------------------ | -------- | ---------------------------------------------------------------------------- | ---------- | | `API_ACCESS_TOKEN` | No | Can be used instead of `--headers Authorization 'Bearer '` | YOUR_TOKEN | ### 1.2 Example usage @@ -115,7 +116,7 @@ separator. Arguments | Name | Required | Description | Example | -|---------------------------|----------------------------|-----------------------------------------------------------------------------------------------|-----------------------| +| ------------------------- | -------------------------- | --------------------------------------------------------------------------------------------- | --------------------- | | `command_or_url` | Yes | The command to spawn the MCP stdio server | uvx mcp-server-fetch | | `--port` | No, random available | The MCP server port to listen on | 8080 | | `--host` | No, `127.0.0.1` by default | The host IP address that the MCP server will listen on | 0.0.0.0 | @@ -256,22 +257,24 @@ services: ## Command line arguments ```bash -usage: mcp-proxy [-h] [-H KEY VALUE] [-e KEY VALUE] [--cwd CWD] [--pass-environment | --no-pass-environment] [--debug | --no-debug] [--port PORT] - [--host HOST] [--stateless | --no-stateless] [--sse-port SSE_PORT] [--sse-host SSE_HOST] +usage: mcp-proxy [-h] [-H KEY VALUE] [--transport {sse,streamablehttp}] [-e KEY VALUE] [--cwd CWD] [--pass-environment | --no-pass-environment] + [--debug | --no-debug] [--port PORT] [--host HOST] [--stateless | --no-stateless] [--sse-port SSE_PORT] [--sse-host SSE_HOST] [--allow-origin ALLOW_ORIGIN [ALLOW_ORIGIN ...]] [command_or_url] [args ...] -Start the MCP proxy in one of two possible modes: as an SSE or stdio client. +Start the MCP proxy in one of two possible modes: as a client or a server. positional arguments: - command_or_url Command or URL to connect to. When a URL, will run an SSE client, otherwise will run the given command and connect as a stdio client. See corresponding options for more details. + command_or_url Command or URL to connect to. When a URL, will run an SSE/StreamableHTTP client, otherwise will run the given command and connect as a stdio client. See corresponding options for more details. options: -h, --help show this help message and exit -SSE client options: +SSE/StreamableHTTP client options: -H, --headers KEY VALUE Headers to pass to the SSE server. Can be used multiple times. + --transport {sse,streamablehttp} + The transport to use for the client. Default is SSE. stdio client options: args Any extra arguments to the command to spawn the server @@ -293,6 +296,7 @@ SSE server options: Examples: mcp-proxy http://localhost:8080/sse + mcp-proxy --transport streamablehttp http://localhost:8080/mcp mcp-proxy --headers Authorization 'Bearer YOUR_TOKEN' http://localhost:8080/sse mcp-proxy --port 8080 -- your-command --arg1 value1 --arg2 value2 mcp-proxy your-command --port 8080 -e KEY VALUE -e ANOTHER_KEY ANOTHER_VALUE diff --git a/pyproject.toml b/pyproject.toml index c74e7a8..eae26a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Topic :: Utilities", "Typing :: Typed", ] -version = "0.6.0" +version = "0.7.0" requires-python = ">=3.10" dependencies = ["mcp>=1.8.0,<2.0.0", "uvicorn>=0.34.0"] diff --git a/src/mcp_proxy/__main__.py b/src/mcp_proxy/__main__.py index 43873ba..76365a8 100644 --- a/src/mcp_proxy/__main__.py +++ b/src/mcp_proxy/__main__.py @@ -17,6 +17,7 @@ from mcp.client.stdio import StdioServerParameters from .mcp_server import MCPServerSettings, run_mcp_server from .sse_client import run_sse_client +from .streamablehttp_client import run_streamablehttp_client # Deprecated env var. Here for backwards compatibility. SSE_URL: t.Final[str | None] = os.getenv( @@ -28,12 +29,11 @@ SSE_URL: t.Final[str | None] = os.getenv( def main() -> None: """Start the client using asyncio.""" parser = argparse.ArgumentParser( - description=( - "Start the MCP proxy in one of two possible modes: as an SSE or stdio client." - ), + description=("Start the MCP proxy in one of two possible modes: as a client or a server."), epilog=( "Examples:\n" " mcp-proxy http://localhost:8080/sse\n" + " mcp-proxy --transport streamablehttp http://localhost:8080/mcp\n" " mcp-proxy --headers Authorization 'Bearer YOUR_TOKEN' http://localhost:8080/sse\n" " mcp-proxy --port 8080 -- your-command --arg1 value1 --arg2 value2\n" " mcp-proxy your-command --port 8080 -e KEY VALUE -e ANOTHER_KEY ANOTHER_VALUE\n" @@ -44,7 +44,7 @@ def main() -> None: parser.add_argument( "command_or_url", help=( - "Command or URL to connect to. When a URL, will run an SSE client, " + "Command or URL to connect to. When a URL, will run an SSE/StreamableHTTP client, " "otherwise will run the given command and connect as a stdio client. " "See corresponding options for more details." ), @@ -52,8 +52,8 @@ def main() -> None: default=SSE_URL, ) - sse_client_group = parser.add_argument_group("SSE client options") - sse_client_group.add_argument( + client_group = parser.add_argument_group("SSE/StreamableHTTP client options") + client_group.add_argument( "-H", "--headers", nargs=2, @@ -62,6 +62,12 @@ def main() -> None: help="Headers to pass to the SSE server. Can be used multiple times.", default=[], ) + client_group.add_argument( + "--transport", + choices=["sse", "streamablehttp"], + default="sse", # For backwards compatibility + help="The transport to use for the client. Default is SSE.", + ) stdio_client_options = parser.add_argument_group("stdio client options") stdio_client_options.add_argument( @@ -155,7 +161,10 @@ def main() -> None: headers = dict(args.headers) if api_access_token := os.getenv("API_ACCESS_TOKEN", None): headers["Authorization"] = f"Bearer {api_access_token}" - asyncio.run(run_sse_client(args.command_or_url, headers=headers)) + if args.transport == "streamablehttp": + asyncio.run(run_streamablehttp_client(args.command_or_url, headers=headers)) + else: + asyncio.run(run_sse_client(args.command_or_url, headers=headers)) return # Start a client connected to the given command, and expose as an SSE server diff --git a/src/mcp_proxy/streamablehttp_client.py b/src/mcp_proxy/streamablehttp_client.py new file mode 100644 index 0000000..740aa81 --- /dev/null +++ b/src/mcp_proxy/streamablehttp_client.py @@ -0,0 +1,30 @@ +"""Create a local server that proxies requests to a remote server over SSE.""" + +from typing import Any + +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamablehttp_client +from mcp.server.stdio import stdio_server + +from .proxy_server import create_proxy_server + + +async def run_streamablehttp_client(url: str, headers: dict[str, Any] | None = None) -> None: + """Run the SSE client. + + Args: + url: The URL to connect to. + headers: Headers for connecting to MCP server. + + """ + async with ( + streamablehttp_client(url=url, headers=headers) as (read, write, _), + ClientSession(read, write) as session, + ): + app = await create_proxy_server(session) + async with stdio_server() as (read_stream, write_stream): + await app.run( + read_stream, + write_stream, + app.create_initialization_options(), + ) diff --git a/uv.lock b/uv.lock index 5e2a392..7f2cda9 100644 --- a/uv.lock +++ b/uv.lock @@ -191,7 +191,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.8.0" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -204,14 +204,14 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/97/0a3e08559557b0ac5799f9fb535fbe5a4e4dcdd66ce9d32e7a74b4d0534d/mcp-1.8.0.tar.gz", hash = "sha256:263dfb700540b726c093f0c3e043f66aded0730d0b51f04eb0a3eb90055fe49b", size = 264641 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/bc/54aec2c334698cc575ca3b3481eed627125fb66544152fa1af927b1a495c/mcp-1.9.1.tar.gz", hash = "sha256:19879cd6dde3d763297617242888c2f695a95dfa854386a6a68676a646ce75e4", size = 316247 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/b2/4ac3bd17b1fdd65658f18de4eb0c703517ee0b483dc5f56467802a9197e0/mcp-1.8.0-py3-none-any.whl", hash = "sha256:889d9d3b4f12b7da59e7a3933a0acadae1fce498bfcd220defb590aa291a1334", size = 119544 }, + { url = "https://files.pythonhosted.org/packages/a6/c0/4ac795585a22a0a2d09cd2b1187b0252d2afcdebd01e10a68bbac4d34890/mcp-1.9.1-py3-none-any.whl", hash = "sha256:2900ded8ffafc3c8a7bfcfe8bc5204037e988e753ec398f371663e6a06ecd9a9", size = 130261 }, ] [[package]] name = "mcp-proxy" -version = "0.6.0" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "mcp" },