diff --git a/docker-compose.yml b/docker-compose.yml index caa7204..9cef690 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: mcp: build: . - command: ["--host", "0.0.0.0"] + command: ["--host", "0.0.0.0", "--transport", "streamable-http"] ports: - 8000:8000 environment: diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index c75c752..08dee2e 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -2,7 +2,7 @@ import click import logging import uvicorn from collections.abc import AsyncIterator -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, AsyncExitStack from dataclasses import dataclass from starlette.applications import Starlette @@ -83,9 +83,19 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): f"Unknown app: {app_name}. Available apps: {list(available_apps.keys())}" ) - mcp_app = mcp.sse_app() if transport == "sse" else mcp.streamable_http_app() + if transport == "sse": + mcp_app = mcp.sse_app() + lifespan = None + else: + mcp_app = mcp.streamable_http_app() - app = Starlette(routes=[Mount("/", app=mcp_app)]) + @asynccontextmanager + async def lifespan(app: Starlette): + async with AsyncExitStack() as stack: + await stack.enter_async_context(mcp.session_manager.run()) + yield + + app = Starlette(routes=[Mount("/", app=mcp_app)], lifespan=lifespan) return app diff --git a/tests/conftest.py b/tests/conftest.py index 155099f..296736f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from typing import Any, AsyncGenerator import pytest from httpx import HTTPStatusError from mcp import ClientSession -from mcp.client.sse import sse_client +from mcp.client.streamable_http import streamablehttp_client from nextcloud_mcp_server.client import NextcloudClient @@ -39,18 +39,18 @@ async def nc_client() -> AsyncGenerator[NextcloudClient, Any]: await client.close() -@pytest.fixture +@pytest.fixture(scope="session") async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]: """ - Fixture to create an MCP client session for integration tests. + Fixture to create an MCP client session for integration tests using streamable-http. """ - logger.info("Creating SSE client") - sse_context = sse_client(url="http://127.0.0.1:8000/sse") + logger.info("Creating Streamable HTTP client") + streamable_context = streamablehttp_client("http://127.0.0.1:8000/mcp") session_context = None try: - read, write = await sse_context.__aenter__() - session_context = ClientSession(read, write) + read_stream, write_stream, _ = await streamable_context.__aenter__() + session_context = ClientSession(read_stream, write_stream) session = await session_context.__aenter__() await session.initialize() logger.info("MCP client session initialized successfully") @@ -71,14 +71,14 @@ async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]: logger.warning(f"Error closing session: {e}") try: - await sse_context.__aexit__(None, None, None) + await streamable_context.__aexit__(None, None, None) except RuntimeError as e: if "cancel scope" in str(e): logger.debug(f"Ignoring cancel scope teardown issue: {e}") else: - logger.warning(f"Error closing SSE client: {e}") + logger.warning(f"Error closing streamable HTTP client: {e}") except Exception as e: - logger.warning(f"Error closing SSE client: {e}") + logger.warning(f"Error closing streamable HTTP client: {e}") @pytest.fixture