|
30 | 30 | import readabilipy.simple_json |
31 | 31 | from mcp import ClientSession |
32 | 32 | from mcp.shared.memory import create_client_server_memory_streams |
33 | | -from mcp.types import InitializeResult |
| 33 | +from mcp.shared.message import SessionMessage |
| 34 | +from mcp.types import ( |
| 35 | + InitializeResult, |
| 36 | + JSONRPCError, |
| 37 | + JSONRPCNotification, |
| 38 | + JSONRPCRequest, |
| 39 | + JSONRPCResponse, |
| 40 | +) |
34 | 41 |
|
35 | 42 | import mcp_server_fetch.server as server_module |
36 | 43 |
|
@@ -152,3 +159,70 @@ def private_tempdir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: |
152 | 159 | each other's pages. |
153 | 160 | """ |
154 | 161 | monkeypatch.setattr(tempfile, "tempdir", str(tmp_path)) |
| 162 | + |
| 163 | + |
| 164 | +async def raw_request( |
| 165 | + method: str, params: dict[str, Any] |
| 166 | +) -> Union[JSONRPCResponse, JSONRPCError]: |
| 167 | + """Send one request exactly as given, bypassing the client's own checks. |
| 168 | +
|
| 169 | + `ClientSession` validates what it sends, so a malformed request (a list as |
| 170 | + a tool name, a number as the arguments) can only reach the server as raw |
| 171 | + JSON-RPC: a 2025-11-25 handshake, then the request, and its response. |
| 172 | + """ |
| 173 | + async with create_client_server_memory_streams() as ( |
| 174 | + (client_read, client_write), |
| 175 | + server_streams, |
| 176 | + ): |
| 177 | + |
| 178 | + @asynccontextmanager |
| 179 | + async def fake_stdio_server() -> AsyncIterator[Any]: |
| 180 | + yield server_streams |
| 181 | + |
| 182 | + async def receive(request_id: int) -> Union[JSONRPCResponse, JSONRPCError]: |
| 183 | + async for item in client_read: |
| 184 | + assert isinstance(item, SessionMessage) |
| 185 | + message = item.message |
| 186 | + if ( |
| 187 | + isinstance(message, (JSONRPCResponse, JSONRPCError)) |
| 188 | + and message.id == request_id |
| 189 | + ): |
| 190 | + return message |
| 191 | + raise AssertionError("the server closed the stream") # pragma: no cover |
| 192 | + |
| 193 | + with patch("mcp.server.mcpserver.server.stdio_server", fake_stdio_server): |
| 194 | + async with anyio.create_task_group() as tg: |
| 195 | + tg.start_soon(lambda: server_module.serve()) |
| 196 | + await client_write.send( |
| 197 | + SessionMessage( |
| 198 | + JSONRPCRequest( |
| 199 | + jsonrpc="2.0", |
| 200 | + id=1, |
| 201 | + method="initialize", |
| 202 | + params={ |
| 203 | + "protocolVersion": "2025-11-25", |
| 204 | + "capabilities": {}, |
| 205 | + "clientInfo": {"name": "raw", "version": "0"}, |
| 206 | + }, |
| 207 | + ) |
| 208 | + ) |
| 209 | + ) |
| 210 | + await receive(1) |
| 211 | + await client_write.send( |
| 212 | + SessionMessage( |
| 213 | + JSONRPCNotification( |
| 214 | + jsonrpc="2.0", method="notifications/initialized" |
| 215 | + ) |
| 216 | + ) |
| 217 | + ) |
| 218 | + await client_write.send( |
| 219 | + SessionMessage( |
| 220 | + JSONRPCRequest( |
| 221 | + jsonrpc="2.0", id=2, method=method, params=params |
| 222 | + ) |
| 223 | + ) |
| 224 | + ) |
| 225 | + response = await receive(2) |
| 226 | + tg.cancel_scope.cancel() |
| 227 | + return response |
| 228 | + raise AssertionError("unreachable") # pragma: no cover |
0 commit comments