"""Tests for Unicode handling in streamable HTTP transport. Verifies that Unicode text is correctly transmitted and received in both directions (server→client and client→server) using the streamable HTTP transport. """ from collections.abc import AsyncIterator from contextlib import asynccontextmanager import httpx import mcp_types as types import pytest from mcp_types import TextContent, Tool from starlette.applications import Starlette from starlette.routing import Mount from mcp.client.session import ClientSession from mcp.client.streamable_http import streamable_http_client from mcp.server import Server, ServerRequestContext from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from tests.interaction.transports import StreamingASGITransport # The in-process app is mounted at this origin purely so URLs are well-formed; nothing listens here. BASE_URL = "http://127.0.0.1:8000" # Test constants with various Unicode characters UNICODE_TEST_STRINGS = { "cyrillic": "Слой хранилища, где располагаются", "cyrillic_short": "Привет мир", "chinese": "你好世界 - 这是一个测试", "japanese": "こんにちは世界 - これはテストです", "korean": "안녕하세요 세계 - 이것은 테스트입니다", "arabic": "مرحبا بالعالم - هذا اختبار", "hebrew": "שלום עולם - זה מבחן", "greek": "Γεια σου κόσμε - αυτό είναι δοκιμή", "emoji": "Hello 👋 World 🌍 - Testing 🧪 Unicode ✨", "math": "∑ ∫ √ ∞ ≠ ≤ ≥ ∈ ∉ ⊆ ⊇", "accented": "Café, naïve, résumé, piñata, Zürich", "mixed": "Hello世界🌍Привет안녕مرحباשלום", "special": "Line\nbreak\ttab\r\nCRLF", "quotes": '«French» „German" "English" 「Japanese」', "currency": "€100 £50 ¥1000 ₹500 ₽200 ¢99", } async def handle_list_tools( ctx: ServerRequestContext, params: types.PaginatedRequestParams | None ) -> types.ListToolsResult: return types.ListToolsResult( tools=[ Tool( name="echo_unicode", description="🔤 Echo Unicode text - Hello 👋 World 🌍 - Testing 🧪 Unicode ✨", input_schema={ "type": "object", "properties": { "text": {"type": "string", "description": "Text to echo back"}, }, "required": ["text"], }, ), ] ) async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult: assert params.name == "echo_unicode" assert params.arguments is not None return types.CallToolResult(content=[TextContent(type="text", text=f"Echo: {params.arguments['text']}")]) async def handle_list_prompts( ctx: ServerRequestContext, params: types.PaginatedRequestParams | None ) -> types.ListPromptsResult: return types.ListPromptsResult( prompts=[ types.Prompt( name="unicode_prompt", description="Unicode prompt - Слой хранилища, где располагаются", arguments=[], ) ] ) async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult: assert params.name == "unicode_prompt" return types.GetPromptResult( messages=[ types.PromptMessage( role="user", content=types.TextContent(type="text", text="Hello世界🌍Привет안녕مرحباשלום"), ) ] ) @asynccontextmanager async def unicode_session() -> AsyncIterator[ClientSession]: """Yield an initialized ClientSession speaking streamable HTTP (SSE responses) to the Unicode test server, entirely in process.""" server = Server( name="unicode_test_server", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool, on_list_prompts=handle_list_prompts, on_get_prompt=handle_get_prompt, ) # SSE response mode, so Unicode rides the SSE event encoding rather than a plain JSON body. session_manager = StreamableHTTPSessionManager(app=server, json_response=False) app = Starlette(routes=[Mount("/mcp", app=session_manager.handle_request)]) async with ( session_manager.run(), # follow_redirects matches the SDK's own client factory; Starlette's Mount 307-redirects # the bare /mcp path to /mcp/. httpx.AsyncClient( transport=StreamingASGITransport(app), base_url=BASE_URL, follow_redirects=True ) as http_client, streamable_http_client(f"{BASE_URL}/mcp", http_client=http_client) as (read_stream, write_stream), ClientSession(read_stream, write_stream) as session, ): await session.initialize() yield session @pytest.mark.anyio async def test_streamable_http_client_unicode_tool_call() -> None: """Test that Unicode text is correctly handled in tool calls via streamable HTTP.""" async with unicode_session() as session: # Test 1: List tools (server→client Unicode in descriptions) tools = await session.list_tools() assert len(tools.tools) == 1 # Check Unicode in tool descriptions echo_tool = tools.tools[0] assert echo_tool.name == "echo_unicode" assert echo_tool.description is not None assert "🔤" in echo_tool.description assert "👋" in echo_tool.description # Test 2: Send Unicode text in tool call (client→server→client) for test_name, test_string in UNICODE_TEST_STRINGS.items(): result = await session.call_tool("echo_unicode", arguments={"text": test_string}) # Verify server correctly received and echoed back Unicode assert len(result.content) == 1 content = result.content[0] assert content.type == "text" assert f"Echo: {test_string}" == content.text, f"Failed for {test_name}" @pytest.mark.anyio async def test_streamable_http_client_unicode_prompts() -> None: """Test that Unicode text is correctly handled in prompts via streamable HTTP.""" async with unicode_session() as session: # Test 1: List prompts (server→client Unicode in descriptions) prompts = await session.list_prompts() assert len(prompts.prompts) == 1 prompt = prompts.prompts[0] assert prompt.name == "unicode_prompt" assert prompt.description is not None assert "Слой хранилища, где располагаются" in prompt.description # Test 2: Get prompt with Unicode content (server→client) result = await session.get_prompt("unicode_prompt", arguments={}) assert len(result.messages) == 1 message = result.messages[0] assert message.role == "user" assert message.content.type == "text" assert message.content.text == "Hello世界🌍Привет안녕مرحباשלום"