Skip to content

Commit 209268a

Browse files
fedirzFedir Zadniprovskyisparfenyuk
authored
feat: expose CORS configuration (sparfenyuk#31)
Allow specifying CORS configuration so that a client can call the proxy from a different host/port. Co-authored-by: Fedir Zadniprovskyi <[email protected]> Co-authored-by: Sergey Parfenyuk <[email protected]>
1 parent 7eb4a09 commit 209268a

File tree

5 files changed

+84
-17
lines changed

5 files changed

+84
-17
lines changed

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ This mode requires passing the URL to the MCP Server SSE endpoint as the first a
5151

5252
Arguments
5353

54-
| Name | Required | Description | Example |
55-
| -------------------- | -------- | --------------------------------------------------------------------- | ----------------------------------------------|
56-
| `command_or_url` | Yes | The MCP server SSE endpoint to connect to | http://example.io/sse |
57-
| `--headers` | No | Headers to use for the MCP server SSE connection | Authorization 'Bearer my-secret-access-token' |
54+
| Name | Required | Description | Example |
55+
| ---------------- | -------- | ------------------------------------------------ | --------------------------------------------- |
56+
| `command_or_url` | Yes | The MCP server SSE endpoint to connect to | http://example.io/sse |
57+
| `--headers` | No | Headers to use for the MCP server SSE connection | Authorization 'Bearer my-secret-access-token' |
5858

5959
Environment Variables
6060

@@ -104,13 +104,14 @@ This mode requires the `--sse-port` argument to be set. The `--sse-host` argumen
104104

105105
Arguments
106106

107-
| Name | Required | Description | Example |
108-
| -------------------- | -------------------------- | ---------------------------------------------------------------- | -------------------- |
109-
| `command_or_url` | Yes | The command to spawn the MCP stdio server | uvx mcp-server-fetch |
110-
| `--sse-port` | No, random available | The SSE server port to listen on | 8080 |
111-
| `--sse-host` | No, `127.0.0.1` by default | The host IP address that the SSE server will listen on | 0.0.0.0 |
112-
| `--env` | No | Additional environment variables to pass to the MCP stdio server | FOO=BAR |
113-
| `--pass-environment` | No | Pass through all environment variables when spawning the server | --no-pass-environment |
107+
| Name | Required | Description | Example |
108+
| -------------------- | -------------------------- | ---------------------------------------------------------------- | --------------------- |
109+
| `command_or_url` | Yes | The command to spawn the MCP stdio server | uvx mcp-server-fetch |
110+
| `--sse-port` | No, random available | The SSE server port to listen on | 8080 |
111+
| `--sse-host` | No, `127.0.0.1` by default | The host IP address that the SSE server will listen on | 0.0.0.0 |
112+
| `--env` | No | Additional environment variables to pass to the MCP stdio server | FOO=BAR |
113+
| `--pass-environment` | No | Pass through all environment variables when spawning the server | --no-pass-environment |
114+
| `--allow-origin` | No | Pass through all environment variables when spawning the server | --allow-cors "\*" |
114115

115116
### 2.2 Example usage
116117

@@ -182,7 +183,9 @@ docker run -t ghcr.io/sparfenyuk/mcp-proxy:v0.3.2-alpine --help
182183
## Command line arguments
183184

184185
```bash
185-
usage: mcp-proxy [-h] [-H KEY VALUE] [-e KEY VALUE] [--sse-port SSE_PORT] [--sse-host SSE_HOST] [--pass-environment] [command_or_url] [args ...]
186+
usage: mcp-proxy [-h] [-H KEY VALUE] [-e KEY VALUE] [--pass-environment | --no-pass-environment] [--sse-port SSE_PORT] [--sse-host SSE_HOST]
187+
[--allow-origin ALLOW_ORIGIN [ALLOW_ORIGIN ...]]
188+
[command_or_url] [args ...]
186189

187190
Start the MCP proxy in one of two possible modes: as an SSE or stdio client.
188191

@@ -206,12 +209,15 @@ stdio client options:
206209
SSE server options:
207210
--sse-port SSE_PORT Port to expose an SSE server on. Default is a random port
208211
--sse-host SSE_HOST Host to expose an SSE server on. Default is 127.0.0.1
212+
--allow-origin ALLOW_ORIGIN [ALLOW_ORIGIN ...]
213+
Allowed origins for the SSE server. Can be used multiple times. Default is no CORS allowed.
209214

210215
Examples:
211216
mcp-proxy http://localhost:8080/sse
212217
mcp-proxy --headers Authorization 'Bearer YOUR_TOKEN' http://localhost:8080/sse
213218
mcp-proxy --sse-port 8080 -- your-command --arg1 value1 --arg2 value2
214219
mcp-proxy your-command --sse-port 8080 -e KEY VALUE -e ANOTHER_KEY ANOTHER_VALUE
220+
mcp-proxy your-command --sse-port 8080 --allow-origin='*'
215221
```
216222
217223
## Testing

codecov.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
codecov:
2+
require_ci_to_pass: true
3+
4+
coverage:
5+
precision: 2
6+
round: down
7+
range: "70...100"
8+
9+
status:
10+
project:
11+
default:
12+
# Maintain overall project coverage
13+
target: auto
14+
threshold: 1%
15+
16+
patch:
17+
default:
18+
# Require 80% coverage on new/modified code
19+
target: 80%
20+
threshold: 5%
21+
22+
changes: false
23+
24+
comment:
25+
layout: "reach,diff,flags,files,footer"
26+
behavior: default
27+
require_changes: false
28+
require_base: false
29+
require_head: true

src/mcp_proxy/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def main() -> None:
3737
" mcp-proxy --headers Authorization 'Bearer YOUR_TOKEN' http://localhost:8080/sse\n"
3838
" mcp-proxy --sse-port 8080 -- your-command --arg1 value1 --arg2 value2\n"
3939
" mcp-proxy your-command --sse-port 8080 -e KEY VALUE -e ANOTHER_KEY ANOTHER_VALUE\n"
40+
" mcp-proxy your-command --sse-port 8080 --allow-origin='*'\n"
4041
),
4142
formatter_class=argparse.RawTextHelpFormatter,
4243
)
@@ -96,6 +97,12 @@ def main() -> None:
9697
default="127.0.0.1",
9798
help="Host to expose an SSE server on. Default is 127.0.0.1",
9899
)
100+
sse_server_group.add_argument(
101+
"--allow-origin",
102+
nargs="+",
103+
default=[],
104+
help="Allowed origins for the SSE server. Can be used multiple times. Default is no CORS allowed.", # noqa: E501
105+
)
99106

100107
args = parser.parse_args()
101108

@@ -135,6 +142,7 @@ def main() -> None:
135142
sse_settings = SseServerSettings(
136143
bind_host=args.sse_host,
137144
port=args.sse_port,
145+
allow_origins=args.allow_origin if len(args.allow_origin) > 0 else None,
138146
)
139147
asyncio.run(run_sse_server(stdio_params, sse_settings))
140148

src/mcp_proxy/sse_server.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from mcp.server import Server
1010
from mcp.server.sse import SseServerTransport
1111
from starlette.applications import Starlette
12+
from starlette.middleware import Middleware
13+
from starlette.middleware.cors import CORSMiddleware
1214
from starlette.requests import Request
1315
from starlette.routing import Mount, Route
1416

@@ -21,10 +23,16 @@ class SseServerSettings:
2123

2224
bind_host: str
2325
port: int
26+
allow_origins: list[str] | None = None
2427
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
2528

2629

27-
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
30+
def create_starlette_app(
31+
mcp_server: Server,
32+
*,
33+
allow_origins: list[str] | None = None,
34+
debug: bool = False,
35+
) -> Starlette:
2836
"""Create a Starlette application that can server the provied mcp server with SSE."""
2937
sse = SseServerTransport("/messages/")
3038

@@ -40,8 +48,20 @@ async def handle_sse(request: Request) -> None:
4048
mcp_server.create_initialization_options(),
4149
)
4250

51+
middleware: list[Middleware] = []
52+
if allow_origins is not None:
53+
middleware.append(
54+
Middleware(
55+
CORSMiddleware,
56+
allow_origins=allow_origins,
57+
allow_methods=["*"],
58+
allow_headers=["*"],
59+
),
60+
)
61+
4362
return Starlette(
4463
debug=debug,
64+
middleware=middleware,
4565
routes=[
4666
Route("/sse", endpoint=handle_sse),
4767
Mount("/messages/", app=sse.handle_post_message),
@@ -64,7 +84,11 @@ async def run_sse_server(
6484
mcp_server = await create_proxy_server(session)
6585

6686
# Bind SSE request handling to MCP server
67-
starlette_app = create_starlette_app(mcp_server, debug=(sse_settings.log_level == "DEBUG"))
87+
starlette_app = create_starlette_app(
88+
mcp_server,
89+
allow_origins=sse_settings.allow_origins,
90+
debug=(sse_settings.log_level == "DEBUG"),
91+
)
6892

6993
# Configure HTTP server
7094
config = uvicorn.Config(

tests/test_sse_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ async def run_in_background(self) -> None:
2727
await asyncio.sleep(1e-3)
2828
yield
2929
finally:
30-
task.cancel()
31-
self.shutdown()
30+
self.should_exit = self.force_exit = True
31+
await task
3232

3333
@property
3434
def url(self) -> str:
@@ -47,7 +47,7 @@ async def test_create_starlette_app() -> None:
4747
async def list_prompts() -> list[types.Prompt]:
4848
return [types.Prompt(name="prompt1")]
4949

50-
app = create_starlette_app(server)
50+
app = create_starlette_app(server, allow_origins=["*"])
5151

5252
config = uvicorn.Config(app, port=0, log_level="info")
5353
server = BackgroundServer(config)

0 commit comments

Comments
 (0)