diff --git a/integtests/conftest.py b/integtests/conftest.py index 10b1ab88..9161a65c 100644 --- a/integtests/conftest.py +++ b/integtests/conftest.py @@ -1,4 +1,5 @@ import dataclasses +import importlib.metadata import json import logging import os @@ -16,10 +17,11 @@ from kbcstorage.client import Client as SyncStorageClient from mcp.server.session import ServerSession from mcp.shared.context import RequestContext +from mcp.types import ClientCapabilities, Implementation, InitializeRequestParams from keboola_mcp_server.clients.client import KeboolaClient from keboola_mcp_server.config import Config, ServerRuntimeInfo -from keboola_mcp_server.mcp import ServerState +from keboola_mcp_server.mcp import ServerState, SessionStateMiddleware from keboola_mcp_server.server import create_server from keboola_mcp_server.workspace import WorkspaceManager @@ -35,6 +37,8 @@ DEV_STORAGE_API_URL_ENV_VAR = 'STORAGE_API_URL' DEV_STORAGE_TOKEN_ENV_VAR = 'KBC_STORAGE_TOKEN' DEV_WORKSPACE_SCHEMA_ENV_VAR = 'KBC_WORKSPACE_SCHEMA' +INTEGTEST_CLIENT_INFO = Implementation(name='integtest/mcp', version=importlib.metadata.version('keboola_mcp_server')) +INTEGTEST_USER_AGENT = f'{INTEGTEST_CLIENT_INFO.name}/{INTEGTEST_CLIENT_INFO.version}' @dataclass(frozen=True) @@ -84,6 +88,44 @@ def env_file_loaded() -> bool: return load_dotenv() +@pytest.fixture(scope='session', autouse=True) +def _patch_fastmcp_client_default_info() -> Generator[None, None, None]: + # Ensure all fastmcp.Client instances in integration tests use a distinct identity + # unless a test intentionally provides a different client_info. + monkeypatch = pytest.MonkeyPatch() + original_init = Client.__init__ + + def _init_with_integtest_client_info(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault('client_info', INTEGTEST_CLIENT_INFO) + original_init(self, *args, **kwargs) + + monkeypatch.setattr(Client, '__init__', _init_with_integtest_client_info) + try: + yield + finally: + monkeypatch.undo() + + +@pytest.fixture(scope='session', autouse=True) +def _patch_session_middleware_user_agent() -> Generator[None, None, None]: + # Force a distinct User-Agent for outbound Keboola API requests during integration tests. + monkeypatch = pytest.MonkeyPatch() + original_get_headers = SessionStateMiddleware._get_headers.__func__ + + def _get_headers_with_integtest_ua( + cls: type[SessionStateMiddleware], runtime_info: ServerRuntimeInfo + ) -> dict[str, Any]: + headers = original_get_headers(cls, runtime_info) + headers['User-Agent'] = INTEGTEST_USER_AGENT + return headers + + monkeypatch.setattr(SessionStateMiddleware, '_get_headers', classmethod(_get_headers_with_integtest_ua)) + try: + yield + finally: + monkeypatch.undo() + + @pytest.fixture(scope='session') def env_init(env_file_loaded: bool, storage_api_token: str, storage_api_url: str, workspace_schema: str) -> bool: # We reset the development environment variables to the values of the integtest environment variables. @@ -300,7 +342,11 @@ def sync_storage_client(storage_api_token: str, storage_api_url: str) -> SyncSto @pytest.fixture def keboola_client(sync_storage_client: SyncStorageClient) -> KeboolaClient: - return KeboolaClient(storage_api_token=sync_storage_client.token, storage_api_url=sync_storage_client.root_url) + return KeboolaClient( + storage_api_token=sync_storage_client.token, + storage_api_url=sync_storage_client.root_url, + headers={'User-Agent': INTEGTEST_USER_AGENT}, + ) @pytest.fixture @@ -332,8 +378,12 @@ def mcp_context( KeboolaClient.STATE_KEY: keboola_client, WorkspaceManager.STATE_KEY: workspace_manager, } - client_context.session.client_params = None - client_context.client_id = None + client_context.session.client_params = InitializeRequestParams( + protocolVersion='1', + capabilities=ClientCapabilities(), + clientInfo=INTEGTEST_CLIENT_INFO, + ) + client_context.client_id = INTEGTEST_USER_AGENT client_context.session_id = None client_context.request_context = mocker.MagicMock(RequestContext) client_context.request_context.lifespan_context = ServerState(mcp_config, ServerRuntimeInfo(transport='stdio')) @@ -351,5 +401,5 @@ def mcp_server(storage_api_url: str, storage_api_token: str, workspace_schema: s @pytest_asyncio.fixture async def mcp_client(mcp_server: FastMCP) -> AsyncGenerator[Client, None]: - async with Client(mcp_server) as client: + async with Client(mcp_server, client_info=INTEGTEST_CLIENT_INFO) as client: yield client diff --git a/integtests/test_errors.py b/integtests/test_errors.py index b22fa19c..af58e34f 100644 --- a/integtests/test_errors.py +++ b/integtests/test_errors.py @@ -9,8 +9,9 @@ import httpx import pytest from fastmcp import Context -from mcp.types import ClientCapabilities, Implementation, InitializeRequestParams +from mcp.types import ClientCapabilities, InitializeRequestParams +from integtests.conftest import INTEGTEST_CLIENT_INFO, INTEGTEST_USER_AGENT from keboola_mcp_server.clients.client import KeboolaClient from keboola_mcp_server.errors import tool_errors from keboola_mcp_server.mcp import CONVERSATION_ID, AggregateError @@ -116,7 +117,7 @@ async def test_event_emitted(self, tool_name: str, event_message: str, event_typ mcp_context.session.client_params = InitializeRequestParams( protocolVersion='1', capabilities=ClientCapabilities(), - clientInfo=Implementation(name='integtest', version='1.2.3'), + clientInfo=INTEGTEST_CLIENT_INFO, ) mcp_context.session.state[CONVERSATION_ID] = '#987654321' unique = uuid.uuid4().hex @@ -145,7 +146,7 @@ async def test_event_emitted(self, tool_name: str, event_message: str, event_typ assert emitted_event['params']['mcpServerContext'] == { 'appEnv': 'DEV', 'version': distribution('keboola_mcp_server').version, - 'userAgent': 'integtest/1.2.3', + 'userAgent': INTEGTEST_USER_AGENT, 'sessionId': 'deadbee', 'serverTransport': 'stdio', 'conversationId': '#987654321', diff --git a/pyproject.toml b/pyproject.toml index aaca9978..7161a9bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "keboola-mcp-server" -version = "1.44.8" +version = "1.44.9" description = "MCP server for interacting with Keboola Connection" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index a763c8f2..0cf23445 100644 --- a/uv.lock +++ b/uv.lock @@ -1223,7 +1223,7 @@ wheels = [ [[package]] name = "keboola-mcp-server" -version = "1.44.8" +version = "1.44.9" source = { editable = "." } dependencies = [ { name = "cryptography" }, @@ -2489,8 +2489,8 @@ name = "taskgroup" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup" }, - { name = "typing-extensions" }, + { name = "exceptiongroup", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } wheels = [