Skip to content

Commit 1b9880b

Browse files
feat: add functionality to start an SSE server to proxy a local stdio server (#11)
* Add CLI for starting the SSE server * Update README.md * Fix lint errors * Update debug signature * Minor fixes --------- Co-authored-by: Sergey Parfenyuk <sergey.parfenyuk@gmail.com>
1 parent c065900 commit 1b9880b

File tree

3 files changed

+163
-24
lines changed

3 files changed

+163
-24
lines changed

README.md

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
## About
1212

13-
Connect to MCP servers that run on SSE transport using the MCP Proxy server.
13+
Connect to MCP servers that run on SSE transport, or expose stdio servers as an SSE server using the MCP Proxy server.
14+
15+
## stdio to SSE
1416

1517
```mermaid
1618
graph LR
@@ -25,6 +27,19 @@ graph LR
2527
> [!TIP]
2628
> As of now, Claude Desktop does not support MCP servers that run on SSE transport. This server is a workaround to enable the support.
2729
30+
## SSE to stdio
31+
32+
```mermaid
33+
graph LR
34+
A["LLM Client"] <--> B["mcp-proxy"]
35+
B <--> C["Local MCP Server"]
36+
37+
style A fill:#ffe6f9,stroke:#333,color:black,stroke-width:2px
38+
style B fill:#e6e6ff,stroke:#333,color:black,stroke-width:2px
39+
style C fill:#e6ffe6,stroke:#333,color:black,stroke-width:2px
40+
```
41+
42+
2843
## Installation
2944

3045
The stable version of the package is available on the PyPI repository. You can install it using the following command:
@@ -60,25 +75,69 @@ Configure Claude Desktop to recognize the MCP server.
6075
6176
2. Add the server configuration
6277

63-
```json
64-
{
65-
"mcpServers": {
66-
"mcp-proxy": {
67-
"command": "mcp-proxy",
68-
"env": {
69-
"SSE_URL": "http://example.io/sse"
70-
}
78+
```json
79+
{
80+
"mcpServers": {
81+
"mcp-proxy": {
82+
"command": "mcp-proxy",
83+
"env": {
84+
"SSE_URL": "http://example.io/sse"
7185
}
72-
}
7386
}
87+
}
88+
}
89+
90+
```
91+
92+
## Detailed Configuration
93+
94+
The MCP Proxy server can support two different approaches for proxying:
95+
- stdio to SSE: To allow clients like Claude Desktop to run this proxy directly. The proxy is started by the LLM Client as a server that proxies to a remote server over SSE.
96+
- SSE to stdio: To allow a client that supports remote SSE servers to access a local stdio server. This proxy opens
97+
a port to listen for SSE requests, then spawns a local stdio server that handles MCP requests.
98+
99+
### stdio to SSE
74100

75-
```
101+
Run a proxy server from stdio that connects to a remote SSE server.
76102

77-
## Advanced Configuration
103+
Arguments
78104

79-
### Environment Variables
105+
| Name | Description |
106+
| ------------------ | ---------------------------------------------------------------------------------- |
107+
| `--sse-url` | Required. The MCP server SSE endpoint to connect to e.g. http://example.io/sse same as environment variable `SSE_URL` |
108+
109+
Environment Variables
80110

81111
| Name | Description |
82112
| ---------------- | ---------------------------------------------------------------------------------- |
83-
| SSE_URL | The MCP server SSE endpoint to connect to e.g. http://example.io/sse |
84-
| API_ACCESS_TOKEN | Added in the `Authorization` header of the HTTP request as a `Bearer` access token |
113+
| `SSE_URL` | The MCP server SSE endpoint to connect to e.g. http://example.io/sse same as `--sse-url` |
114+
| `API_ACCESS_TOKEN` | Added in the `Authorization` header of the HTTP request as a `Bearer` access token |
115+
116+
117+
Example usage:
118+
119+
```bash
120+
uv run mcp-proxy --sse-url=http://example.io/sse
121+
```
122+
123+
124+
### SSE to stdio
125+
126+
Run a proxy server exposing an SSE server that connects to a local stdio server. This allows remote connections to the stdio server.
127+
128+
Arguments
129+
130+
| Name | Description |
131+
| ------------------ | ---------------------------------------------------------------------------------- |
132+
| `--sse-port` | Required. The SSE server port to listen to e.g. `8080` |
133+
| `--sse-host` | Optional. The host IP address that the SSE server will listen on e.g. `0.0.0.0`. By default only listens on localhost. |
134+
| command | Required. The path for the MCP stdio server command line. |
135+
| arg1 arg2 ... | Optional. Additional arguments to the MCP stdio server command line program. |
136+
137+
Example usage:
138+
139+
```bash
140+
uv run mcp-proxy --sse-port=8080 -e FOO=BAR -- /path/to/command arg1 arg2
141+
```
142+
143+
This will start an MCP server that can be connected to at `http://127.0.0.1:8080/sse`

src/mcp_proxy/__main__.py

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,104 @@
66
77
"""
88

9+
import argparse
910
import asyncio
1011
import logging
1112
import os
13+
import sys
1214
import typing as t
1315

16+
from mcp.client.stdio import StdioServerParameters
17+
1418
from .sse_client import run_sse_client
19+
from .sse_server import SseServerSettings, run_sse_server
1520

1621
logging.basicConfig(level=logging.DEBUG)
17-
SSE_URL: t.Final[str] = os.getenv("SSE_URL", "")
22+
SSE_URL: t.Final[str | None] = os.getenv("SSE_URL", None)
1823
API_ACCESS_TOKEN: t.Final[str | None] = os.getenv("API_ACCESS_TOKEN", None)
1924

20-
if not SSE_URL:
21-
raise ValueError("SSE_URL environment variable is not set")
22-
2325

2426
def main() -> None:
2527
"""Start the client using asyncio."""
26-
asyncio.run(run_sse_client(SSE_URL, api_access_token=API_ACCESS_TOKEN))
28+
parser = argparse.ArgumentParser()
29+
parser.add_argument(
30+
"command_or_url",
31+
help=(
32+
"Command or URL to connect to. When a URL, will run an SSE client "
33+
"to connect to the server, otherwise will run the command and "
34+
"connect as a stdio client. Can also be set as environment variable SSE_URL."
35+
),
36+
nargs="?", # Required below to allow for coming form env var
37+
default=SSE_URL,
38+
)
39+
40+
sse_client_group = parser.add_argument_group("SSE client options")
41+
sse_client_group.add_argument(
42+
"--api-access-token",
43+
default=API_ACCESS_TOKEN,
44+
help=(
45+
"Access token Authorization header passed by the client to the SSE "
46+
"server. Can also be set as environment variable API_ACCESS_TOKEN."
47+
),
48+
)
49+
50+
stdio_client_options = parser.add_argument_group("stdio client options")
51+
stdio_client_options.add_argument(
52+
"args",
53+
nargs="*",
54+
help="Arguments to the command to run to spawn the server",
55+
)
56+
stdio_client_options.add_argument(
57+
"-e",
58+
"--env",
59+
nargs=2,
60+
action="append",
61+
metavar=("KEY", "VALUE"),
62+
help="Environment variables used when spawning the server. Can be used multiple times.",
63+
default=[],
64+
)
65+
66+
sse_server_group = parser.add_argument_group("SSE server options")
67+
sse_server_group.add_argument(
68+
"--sse-port",
69+
type=int,
70+
default=None,
71+
help="Port to expose an SSE server on",
72+
)
73+
sse_server_group.add_argument(
74+
"--sse-host",
75+
default="127.0.0.1",
76+
help="Host to expose an SSE server on",
77+
)
78+
79+
args = parser.parse_args()
80+
81+
if not args.command_or_url:
82+
parser.print_help()
83+
sys.exit(1)
84+
85+
if (
86+
SSE_URL
87+
or args.command_or_url.startswith("http://")
88+
or args.command_or_url.startswith("https://")
89+
):
90+
# Start a client connected to the SSE server, and expose as a stdio server
91+
logging.debug("Starting SSE client and stdio server")
92+
asyncio.run(run_sse_client(args.command_or_url, api_access_token=API_ACCESS_TOKEN))
93+
return
94+
95+
# Start a client connected to the given command, and expose as an SSE server
96+
logging.debug("Starting stdio client and SSE server")
97+
stdio_params = StdioServerParameters(
98+
command=args.command_or_url,
99+
args=args.args,
100+
env=dict(args.env),
101+
)
102+
sse_settings = SseServerSettings(
103+
bind_host=args.sse_host,
104+
port=args.sse_port,
105+
)
106+
asyncio.run(run_sse_server(stdio_params, sse_settings))
27107

28108

29109
if __name__ == "__main__":

src/mcp_proxy/sse_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
class SseServerSettings:
2020
"""Settings for the server."""
2121

22-
bind_host: str = "127.0.0.1"
23-
port: int = 8000
22+
bind_host: str
23+
port: int
2424
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
2525

2626

27-
def create_starlette_app(mcp_server: Server, debug: bool | None = None) -> Starlette:
27+
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
2828
"""Create a Starlette application that can server the provied mcp server with SSE."""
2929
sse = SseServerTransport("/messages/")
3030

@@ -64,7 +64,7 @@ async def run_sse_server(
6464
mcp_server = await create_proxy_server(session)
6565

6666
# Bind SSE request handling to MCP server
67-
starlette_app = await create_starlette_app(mcp_server, sse_settings.log_level == "DEBUG")
67+
starlette_app = create_starlette_app(mcp_server, debug=(sse_settings.log_level == "DEBUG"))
6868

6969
# Configure HTTP server
7070
config = uvicorn.Config(

0 commit comments

Comments
 (0)