Merge pull request #65 from open-webui/dev

0.0.11
This commit is contained in:
Tim Jaeryang Baek 2025-04-12 12:24:25 -07:00 committed by GitHub
commit 3b5f4fe17e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 74 additions and 28 deletions

View File

@ -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

View File

@ -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
}
}
```
@ -110,7 +119,7 @@ To contribute or run tests locally:
2. **Run tests:**
```bash
pytest
uv run pytest
```

View File

@ -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" }

View File

@ -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", "--server-type", help="Server type")
] = "stdio",
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,

View File

@ -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
@ -65,13 +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", {})
args = args if isinstance(args, list) else [args]
api_dependency = getattr(app.state, "api_dependency", None)
if not command:
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):
@ -79,19 +84,25 @@ async def lifespan(app: FastAPI):
route.app.router.lifespan_context(route.app), # noqa
)
yield
else:
server_params = StdioServerParameters(
command=command,
args=args,
env={**env},
)
if server_type == "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 server_type == "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(
@ -104,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"
@ -135,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)
@ -166,9 +187,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.server_type = "sse"
sub_app.state.args = server_cfg["url"]
sub_app.state.api_dependency = api_dependency

View File

@ -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 = {}