From e05e4202eb124e6e531c5e4d4a435296b5f70b2b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 10 Apr 2025 19:01:31 -0700 Subject: [PATCH 1/6] refac --- {tests => src/mcpo/tests}/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {tests => src/mcpo/tests}/test_main.py (99%) diff --git a/tests/test_main.py b/src/mcpo/tests/test_main.py similarity index 99% rename from tests/test_main.py rename to src/mcpo/tests/test_main.py index 94d4454..db10617 100644 --- a/tests/test_main.py +++ b/src/mcpo/tests/test_main.py @@ -2,7 +2,7 @@ import pytest from pydantic import BaseModel, Field from typing import Any, List, Dict -from src.mcpo.utils.main import _process_schema_property +from mcpo.utils.main import _process_schema_property _model_cache = {} From 917a4f57ced02b9c52ad95672358a24d6922cec7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 10 Apr 2025 19:01:56 -0700 Subject: [PATCH 2/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41355ac..d1e5b23 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ To contribute or run tests locally: 2. **Run tests:** ```bash - pytest + uv run pytest ``` From 6fbc97e506941a976bb3a0f472d3be18b81a73b1 Mon Sep 17 00:00:00 2001 From: Azam Janah Date: Sat, 12 Apr 2025 08:15:45 +0800 Subject: [PATCH 3/6] Added ability to connect to SSE MCP server - use format below to connect to SSE MCP server uv run mcpo -- sse http://localhost:8001/sse - use format below for config.json to connect to SSE MCP server { "mcpServers": { "mcp_sse": { "url": "http://127.0.0.1:8001/sse" }, "mcp_stdio": { "command": "python", "args": [ "mcp_stdio.py" ] } } } --- src/mcpo/main.py | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/mcpo/main.py b/src/mcpo/main.py index 2fe872e..7cb1560 100644 --- a/src/mcpo/main.py +++ b/src/mcpo/main.py @@ -11,7 +11,7 @@ from starlette.routing import Mount from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client - +from mcp.client.sse import sse_client from mcpo.utils.main import get_model_fields, get_tool_handler from mcpo.utils.auth import get_verify_api_key @@ -69,9 +69,12 @@ async def lifespan(app: FastAPI): args = getattr(app.state, "args", []) env = getattr(app.state, "env", {}) + mcptype = "sse" if command == "sse" else "stdio" + args = args if isinstance(args, list) else [args] + api_dependency = getattr(app.state, "api_dependency", None) - if not command: + if (mcptype == "stdio" and not command) or (mcptype == "sse" and not args[0]): async with AsyncExitStack() as stack: for route in app.routes: if isinstance(route, Mount) and isinstance(route.app, FastAPI): @@ -81,17 +84,24 @@ async def lifespan(app: FastAPI): yield else: - server_params = StdioServerParameters( - command=command, - args=args, - env={**env}, - ) + if mcptype == "stdio": + server_params = StdioServerParameters( + command=command, + args=args, + env={**env}, + ) - async with stdio_client(server_params) as (reader, writer): - async with ClientSession(reader, writer) as session: - app.state.session = session - await create_dynamic_endpoints(app, api_dependency=api_dependency) - yield + async with stdio_client(server_params) as (reader, writer): + async with ClientSession(reader, writer) as session: + app.state.session = session + await create_dynamic_endpoints(app, api_dependency=api_dependency) + yield + if mcptype == "sse": + async with sse_client(url=args[0]) as (reader, writer): + async with ClientSession(reader, writer) as session: + app.state.session = session + await create_dynamic_endpoints(app, api_dependency=api_dependency) + yield async def run( @@ -166,9 +176,15 @@ async def run( allow_headers=["*"], ) - sub_app.state.command = server_cfg["command"] - sub_app.state.args = server_cfg.get("args", []) - sub_app.state.env = {**os.environ, **server_cfg.get("env", {})} + if server_cfg.get("command"): + # stdio + sub_app.state.command = server_cfg["command"] + sub_app.state.args = server_cfg.get("args", []) + sub_app.state.env = {**os.environ, **server_cfg.get("env", {})} + if server_cfg.get("url"): + # SSE + sub_app.state.command = "sse" + sub_app.state.args = server_cfg["url"] sub_app.state.api_dependency = api_dependency From 77fbb9a727a1e005b2527b714ee6e83ee92b7643 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 12 Apr 2025 12:12:55 -0700 Subject: [PATCH 4/6] refac --- src/mcpo/__init__.py | 14 +++++++++----- src/mcpo/main.py | 31 +++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/mcpo/__init__.py b/src/mcpo/__init__.py index 9d31bd2..9f62c52 100644 --- a/src/mcpo/__init__.py +++ b/src/mcpo/__init__.py @@ -33,7 +33,10 @@ def main( Optional[str], typer.Option("--env-path", help="Path to environment variables file"), ] = None, - config: Annotated[ + server_type: Annotated[ + Optional[str], typer.Option("--type", help="Server type") + ] = None, + config_path: Annotated[ Optional[str], typer.Option("--config", "-c", help="Config file path") ] = None, name: Annotated[ @@ -56,7 +59,7 @@ def main( ] = None, ): server_command = None - if not config: + if not config_path: # Find the position of "--" if "--" not in sys.argv: typer.echo("Usage: mcpo --host 0.0.0.0 --port 8000 -- your_mcp_command") @@ -71,8 +74,8 @@ def main( from mcpo.main import run - if config: - print("Starting MCP OpenAPI Proxy with config file:", config) + if config_path: + print("Starting MCP OpenAPI Proxy with config file:", config_path) else: print( f"Starting MCP OpenAPI Proxy on {host}:{port} with command: {' '.join(server_command)}" @@ -114,7 +117,8 @@ def main( port, api_key=api_key, cors_allow_origins=cors_allow_origins, - config=config, + server_type=server_type, + config_path=config_path, name=name, description=description, version=version, diff --git a/src/mcpo/main.py b/src/mcpo/main.py index 7cb1560..38d6e4c 100644 --- a/src/mcpo/main.py +++ b/src/mcpo/main.py @@ -65,16 +65,18 @@ async def create_dynamic_endpoints(app: FastAPI, api_dependency=None): @asynccontextmanager async def lifespan(app: FastAPI): + server_type = getattr(app.state, "server_type", "stdio") command = getattr(app.state, "command", None) args = getattr(app.state, "args", []) env = getattr(app.state, "env", {}) - mcptype = "sse" if command == "sse" else "stdio" args = args if isinstance(args, list) else [args] - api_dependency = getattr(app.state, "api_dependency", None) - if (mcptype == "stdio" and not command) or (mcptype == "sse" and not args[0]): + if (server_type == "stdio" and not command) or ( + server_type == "sse" and not args[0] + ): + # Main app lifespan (when config_path is provided) async with AsyncExitStack() as stack: for route in app.routes: if isinstance(route, Mount) and isinstance(route.app, FastAPI): @@ -82,9 +84,8 @@ async def lifespan(app: FastAPI): route.app.router.lifespan_context(route.app), # noqa ) yield - else: - if mcptype == "stdio": + if server_type == "stdio": server_params = StdioServerParameters( command=command, args=args, @@ -96,7 +97,7 @@ async def lifespan(app: FastAPI): app.state.session = session await create_dynamic_endpoints(app, api_dependency=api_dependency) yield - if mcptype == "sse": + if server_type == "sse": async with sse_client(url=args[0]) as (reader, writer): async with ClientSession(reader, writer) as session: app.state.session = session @@ -114,10 +115,14 @@ async def run( # Server API Key api_dependency = get_verify_api_key(api_key) if api_key else None - # MCP Config - config_path = kwargs.get("config") + # MCP Server + server_type = kwargs.get("server_type") # "stdio" or "sse" or "http" server_command = kwargs.get("server_command") + # MCP Config + config_path = kwargs.get("config_path") + + # mcpo server name = kwargs.get("name") or "MCP OpenAPI Proxy" description = ( kwargs.get("description") or "Automatically generated API from MCP Tool Schemas" @@ -145,12 +150,18 @@ async def run( allow_headers=["*"], ) - if server_command: + if server_type == "sse": + main_app.state.server_type = "sse" + main_app.state.args = server_command[0] + main_app.state.api_dependency = api_dependency + + elif server_command: main_app.state.command = server_command[0] main_app.state.args = server_command[1:] main_app.state.env = os.environ.copy() main_app.state.api_dependency = api_dependency + elif config_path: with open(config_path, "r") as f: config_data = json.load(f) @@ -183,7 +194,7 @@ async def run( sub_app.state.env = {**os.environ, **server_cfg.get("env", {})} if server_cfg.get("url"): # SSE - sub_app.state.command = "sse" + sub_app.state.server_type = "sse" sub_app.state.args = server_cfg["url"] sub_app.state.api_dependency = api_dependency From 650912c5ee577b1b792dbd94418dd669322a152a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 12 Apr 2025 12:19:30 -0700 Subject: [PATCH 5/6] doc: readme --- README.md | 11 ++++++++++- src/mcpo/__init__.py | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1e5b23..5ea45d1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ pip install mcpo mcpo --port 8000 --api-key "top-secret" -- your_mcp_server_command ``` +To use an SSE-compatible MCP server, simply specify the server type and endpoint: + +```bash +mcpo --port 8000 --api-key "top-secret" --server-type "sse" -- http://127.0.0.1:8001/sse +``` + You can also run mcpo via Docker with no installation: ```bash @@ -78,7 +84,10 @@ Example config.json: "time": { "command": "uvx", "args": ["mcp-server-time", "--local-timezone=America/New_York"] - } + }, + "mcp_sse": { + "url": "http://127.0.0.1:8001/sse" + } // SSE MCP Server } } ``` diff --git a/src/mcpo/__init__.py b/src/mcpo/__init__.py index 9f62c52..534937a 100644 --- a/src/mcpo/__init__.py +++ b/src/mcpo/__init__.py @@ -34,8 +34,8 @@ def main( typer.Option("--env-path", help="Path to environment variables file"), ] = None, server_type: Annotated[ - Optional[str], typer.Option("--type", help="Server type") - ] = None, + Optional[str], typer.Option("--type", "--server-type", help="Server type") + ] = "stdio", config_path: Annotated[ Optional[str], typer.Option("--config", "-c", help="Config file path") ] = None, From 6d1b8829bf51a74cfa20c5392366508c39cd99b8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 12 Apr 2025 12:24:16 -0700 Subject: [PATCH 6/6] doc: changelog --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8487915..188aeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.11] - 2025-04-12 + +### Added + +- 🌊 **SSE-Based MCP Server Support**: mcpo now supports SSE (Server-Sent Events) MCP servers out of the box—just pass 'mcpo --server-type "sse" -- http://127.0.0.1:8001/sse' when launching or use the standard "url" field in your config for seamless real-time integration with streaming MCP endpoints; see the README for full examples and enhanced workflows with live progress, event pushes, and interactive updates. + ## [0.0.10] - 2025-04-10 ### Added diff --git a/pyproject.toml b/pyproject.toml index 758d4ff..02cbda7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcpo" -version = "0.0.10" +version = "0.0.11" description = "A simple, secure MCP-to-OpenAPI proxy server" authors = [ { name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }