Merge pull request 'fix: serve MCP over Streamable HTTP instead of legacy SSE' (#58) from fix/streamable-http-transport into main
Main Release / build (push) Canceled after 0s
Main Release / tag (push) Canceled after 0s
Main Release / integration-test (push) Canceled after 1m8s
Main Release / hadolint (push) Canceled after 1m12s
Main Release / test (push) Canceled after 1m10s
Docker Build and Push / test (push) Successful in 2m14s
Renovate / renovate (push) Successful in 1m32s
Docker Build and Push / integration-test (push) Failing after 11m3s
Docker Build and Push / build (push) Skipped
Main Release / build (push) Canceled after 0s
Main Release / tag (push) Canceled after 0s
Main Release / integration-test (push) Canceled after 1m8s
Main Release / hadolint (push) Canceled after 1m12s
Main Release / test (push) Canceled after 1m10s
Docker Build and Push / test (push) Successful in 2m14s
Renovate / renovate (push) Successful in 1m32s
Docker Build and Push / integration-test (push) Failing after 11m3s
Docker Build and Push / build (push) Skipped
Reviewed-on: #58
This commit was merged in pull request #58.
This commit is contained in:
commit
dd41603a0b
3 files changed
+49
-5
No files matched your search
+1
-1
@@ -15,7 +15,7 @@ services:
|
|||||||
dockerfile: pkg/Dockerfile
|
dockerfile: pkg/Dockerfile
|
||||||
container_name: mcp_maildir_server
|
container_name: mcp_maildir_server
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8000:8000" # Expose the MCP HTTP (SSE) server
|
- "127.0.0.1:8000:8000" # Expose the MCP Streamable HTTP server (/mcp)
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
+21
-4
@@ -9,6 +9,7 @@ from typing import Any, Optional
|
|||||||
import qdrant_client as qdrant_pkg
|
import qdrant_client as qdrant_pkg
|
||||||
from dateutil import parser as date_parser
|
from dateutil import parser as date_parser
|
||||||
from email.utils import parseaddr
|
from email.utils import parseaddr
|
||||||
|
import uvicorn
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
@@ -340,6 +341,23 @@ def read_email(message_id: str):
|
|||||||
return {"error": f"read_email failed: {exc}"}
|
return {"error": f"read_email failed: {exc}"}
|
||||||
|
|
||||||
|
|
||||||
|
# Streamable HTTP is the transport supported by current MCP clients (OpenCode,
|
||||||
|
# Claude, ...). The legacy SSE transport is deprecated and is also a POST
|
||||||
|
# target for clients that only speak Streamable HTTP, which answers 405.
|
||||||
|
MCP_HOST = "0.0.0.0"
|
||||||
|
MCP_PORT = 8000
|
||||||
|
MCP_HTTP_PATH = "/mcp"
|
||||||
|
|
||||||
|
|
||||||
|
def build_app():
|
||||||
|
"""Builds the ASGI app serving the MCP endpoint over Streamable HTTP.
|
||||||
|
|
||||||
|
The MCP endpoint lives at MCP_HTTP_PATH; the custom route declared above
|
||||||
|
keeps serving GET /health for the Kubernetes probes.
|
||||||
|
"""
|
||||||
|
return mcp.http_app(path=MCP_HTTP_PATH)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logger.info("Initializing models before starting server...")
|
logger.info("Initializing models before starting server...")
|
||||||
try:
|
try:
|
||||||
@@ -347,7 +365,6 @@ if __name__ == "__main__":
|
|||||||
logger.info("Models loaded successfully.")
|
logger.info("Models loaded successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load models: {e}")
|
logger.error(f"Failed to load models: {e}")
|
||||||
|
|
||||||
logger.info("Starting SSE server on 0.0.0.0:8000...")
|
logger.info(f"Starting Streamable HTTP server on {MCP_HOST}:{MCP_PORT}{MCP_HTTP_PATH}...")
|
||||||
# Start the MCP server using SSE (Server-Sent Events) over HTTP
|
uvicorn.run(build_app(), host=MCP_HOST, port=MCP_PORT)
|
||||||
mcp.run(transport="sse", host="0.0.0.0", port=8000)
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""Transport-level tests for the mcp-maildir ASGI app.
|
||||||
|
|
||||||
|
Current MCP clients (OpenCode, Claude, ...) only speak Streamable HTTP: they
|
||||||
|
POST JSON-RPC to the configured URL. Serving the legacy SSE transport alone
|
||||||
|
makes those clients fail with `405 Method Not Allowed`, so the app built by
|
||||||
|
`server.build_app()` must expose the MCP endpoint at /mcp.
|
||||||
|
|
||||||
|
Env vars are set in tests/conftest.py (shared session-wide).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from server import build_app, MCP_HTTP_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def app_paths() -> set:
|
||||||
|
return {getattr(route, "path", None) for route in build_app().routes}
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransport:
|
||||||
|
def test_mcp_endpoint_is_streamable_http(self):
|
||||||
|
assert MCP_HTTP_PATH == "/mcp"
|
||||||
|
assert MCP_HTTP_PATH in app_paths()
|
||||||
|
|
||||||
|
def test_health_endpoint_is_exposed_for_probes(self):
|
||||||
|
assert "/health" in app_paths()
|
||||||
|
|
||||||
|
def test_legacy_sse_endpoint_is_gone(self):
|
||||||
|
assert "/sse" not in app_paths()
|
||||||
Reference in new issue
Block a user