From 709059234e8eea1ce844070cb08aa9fe0e392540 Mon Sep 17 00:00:00 2001 From: Lovish Arora <46993225+lavish0000@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:47:20 +0100 Subject: [PATCH 1/4] fix(session): return METHOD_NOT_FOUND for unknown request methods --- src/mcp/client/session.py | 12 ++++- src/mcp/server/session.py | 7 +++ src/mcp/shared/session.py | 27 +++++++++- tests/issues/test_1561_invalid_method_code.py | 51 +++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 tests/issues/test_1561_invalid_method_code.py diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index a0ca751bd..7df2ea6de 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -12,11 +12,17 @@ from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers from mcp.shared._context import RequestContext from mcp.shared.message import SessionMessage -from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder +from mcp.shared.session import ( + BaseSession, + ProgressFnT, + RequestResponder, + request_methods_for_union, +) from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS from mcp.types._types import RequestParamsMeta DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0") +KNOWN_SERVER_REQUEST_METHODS = request_methods_for_union(types.ServerRequest) logger = logging.getLogger("client") @@ -141,6 +147,10 @@ def __init__( def _receive_request_adapter(self) -> TypeAdapter[types.ServerRequest]: return types.server_request_adapter + @property + def _known_request_methods(self) -> frozenset[str]: + return KNOWN_SERVER_REQUEST_METHODS + @property def _receive_notification_adapter(self) -> TypeAdapter[types.ServerNotification]: return types.server_notification_adapter diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index 759d2131a..e90837d0b 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -47,6 +47,7 @@ async def handle_list_prompts(ctx: RequestContext, params) -> ListPromptsResult: from mcp.shared.session import ( BaseSession, RequestResponder, + request_methods_for_union, ) from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS @@ -63,6 +64,8 @@ class InitializationState(Enum): RequestResponder[types.ClientRequest, types.ServerResult] | types.ClientNotification | Exception ) +KNOWN_CLIENT_REQUEST_METHODS = request_methods_for_union(types.ClientRequest) + class ServerSession( BaseSession[ @@ -100,6 +103,10 @@ def __init__( def _receive_request_adapter(self) -> TypeAdapter[types.ClientRequest]: return types.client_request_adapter + @property + def _known_request_methods(self) -> frozenset[str]: + return KNOWN_CLIENT_REQUEST_METHODS + @property def _receive_notification_adapter(self) -> TypeAdapter[types.ClientNotification]: return types.client_notification_adapter diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index b617d702f..74ee5c440 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -4,7 +4,7 @@ from collections.abc import Callable from contextlib import AsyncExitStack from types import TracebackType -from typing import Any, Generic, Protocol, TypeVar +from typing import Any, Generic, Protocol, TypeVar, get_args import anyio from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -17,6 +17,7 @@ from mcp.types import ( CONNECTION_CLOSED, INVALID_PARAMS, + METHOD_NOT_FOUND, REQUEST_TIMEOUT, CancelledNotification, ClientNotification, @@ -45,6 +46,16 @@ RequestId = str | int +def request_methods_for_union(request_union: Any) -> frozenset[str]: + methods: set[str] = set() + for request_type in get_args(request_union): + field = getattr(request_type, "model_fields", {}).get("method") + default = getattr(field, "default", None) + if isinstance(default, str): + methods.add(default) + return frozenset(methods) + + class ProgressFnT(Protocol): """Protocol for progress notification callbacks.""" @@ -326,6 +337,10 @@ def _receive_request_adapter(self) -> TypeAdapter[ReceiveRequestT]: """Each subclass must provide its own request adapter.""" raise NotImplementedError + @property + def _known_request_methods(self) -> frozenset[str]: + return frozenset() + @property def _receive_notification_adapter(self) -> TypeAdapter[ReceiveNotificationT]: raise NotImplementedError @@ -360,10 +375,18 @@ async def _receive_loop(self) -> None: # response instead of crashing the server logging.warning("Failed to validate request", exc_info=True) logging.debug(f"Message that failed validation: {message.message}") + if message.message.method not in self._known_request_methods: + error = ErrorData(code=METHOD_NOT_FOUND, message="Method not found") + else: + error = ErrorData( + code=INVALID_PARAMS, + message="Invalid request parameters", + data="", + ) error_response = JSONRPCError( jsonrpc="2.0", id=message.message.id, - error=ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data=""), + error=error, ) session_message = SessionMessage(message=error_response) await self._write_stream.send(session_message) diff --git a/tests/issues/test_1561_invalid_method_code.py b/tests/issues/test_1561_invalid_method_code.py new file mode 100644 index 000000000..d077259a5 --- /dev/null +++ b/tests/issues/test_1561_invalid_method_code.py @@ -0,0 +1,51 @@ +"""Test for issue #1561: unknown methods should return METHOD_NOT_FOUND.""" + +import anyio +import pytest + +from mcp.server.models import InitializationOptions +from mcp.server.session import ServerSession +from mcp.shared.message import SessionMessage +from mcp.types import METHOD_NOT_FOUND, JSONRPCError, JSONRPCRequest, ServerCapabilities + + +@pytest.mark.anyio +async def test_invalid_method_returns_method_not_found() -> None: + read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10) + write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10) + + try: + async with ServerSession( + read_stream=read_receive_stream, + write_stream=write_send_stream, + init_options=InitializationOptions( + server_name="test_server", + server_version="1.0.0", + capabilities=ServerCapabilities(), + ), + ): + await read_send_stream.send( + SessionMessage( + message=JSONRPCRequest( + jsonrpc="2.0", + id=1, + method="invalid/method", + params={}, + ) + ) + ) + + await anyio.sleep(0.1) + + response_message = write_receive_stream.receive_nowait() + response = response_message.message + + assert isinstance(response, JSONRPCError) + assert response.id == 1 + assert response.error.code == METHOD_NOT_FOUND + assert response.error.message == "Method not found" + finally: # pragma: no cover + await read_send_stream.aclose() + await write_send_stream.aclose() + await read_receive_stream.aclose() + await write_receive_stream.aclose() From 5a295330e73b5e11fd32054e50af23723404c2d7 Mon Sep 17 00:00:00 2001 From: Lovish Arora <46993225+lavish0000@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:43:39 +0100 Subject: [PATCH 2/4] test(session): cover known request method helpers --- tests/issues/test_1561_invalid_method_code.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/issues/test_1561_invalid_method_code.py b/tests/issues/test_1561_invalid_method_code.py index d077259a5..5c86d42c3 100644 --- a/tests/issues/test_1561_invalid_method_code.py +++ b/tests/issues/test_1561_invalid_method_code.py @@ -2,10 +2,14 @@ import anyio import pytest +from pydantic import BaseModel +from mcp import types +from mcp.client.session import KNOWN_SERVER_REQUEST_METHODS, ClientSession from mcp.server.models import InitializationOptions from mcp.server.session import ServerSession from mcp.shared.message import SessionMessage +from mcp.shared.session import BaseSession, request_methods_for_union from mcp.types import METHOD_NOT_FOUND, JSONRPCError, JSONRPCRequest, ServerCapabilities @@ -49,3 +53,64 @@ async def test_invalid_method_returns_method_not_found() -> None: await write_send_stream.aclose() await read_receive_stream.aclose() await write_receive_stream.aclose() + + +class MissingDefaultMethodRequest(BaseModel): + jsonrpc: str = "2.0" + id: int = 1 + method: str + + +def test_request_methods_for_union_ignores_non_literal_defaults() -> None: + methods = request_methods_for_union(types.ServerRequest | MissingDefaultMethodRequest) + assert methods == KNOWN_SERVER_REQUEST_METHODS + + +@pytest.mark.anyio +async def test_client_session_known_request_methods_match_server_request_union() -> None: + read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10) + write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10) + + try: + session = ClientSession(read_stream=read_receive_stream, write_stream=write_send_stream) + assert session._known_request_methods == KNOWN_SERVER_REQUEST_METHODS + finally: # pragma: no cover + await read_send_stream.aclose() + await write_send_stream.aclose() + await read_receive_stream.aclose() + await write_receive_stream.aclose() + + +class DummyBaseSession( + BaseSession[ + types.ClientRequest, + types.ClientNotification, + types.ClientResult, + types.ServerRequest, + types.ServerNotification, + ] +): + @property + def _receive_request_adapter(self): + return types.server_request_adapter + + @property + def _receive_notification_adapter(self): + return types.server_notification_adapter + + +@pytest.mark.anyio +async def test_base_session_known_request_methods_default_to_empty() -> None: + read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10) + write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10) + + try: + session = DummyBaseSession(read_stream=read_receive_stream, write_stream=write_send_stream) + assert session._known_request_methods == frozenset() + assert session._receive_request_adapter is types.server_request_adapter + assert session._receive_notification_adapter is types.server_notification_adapter + finally: # pragma: no cover + await read_send_stream.aclose() + await write_send_stream.aclose() + await read_receive_stream.aclose() + await write_receive_stream.aclose() From e22de00db408d521c8e898d8b50c15750b07b677 Mon Sep 17 00:00:00 2001 From: Lovish Arora <46993225+lavish0000@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:46:35 +0100 Subject: [PATCH 3/4] test(session): drop unnecessary coverage pragmas --- tests/issues/test_1561_invalid_method_code.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/issues/test_1561_invalid_method_code.py b/tests/issues/test_1561_invalid_method_code.py index 5c86d42c3..2f9c08aa8 100644 --- a/tests/issues/test_1561_invalid_method_code.py +++ b/tests/issues/test_1561_invalid_method_code.py @@ -48,7 +48,7 @@ async def test_invalid_method_returns_method_not_found() -> None: assert response.id == 1 assert response.error.code == METHOD_NOT_FOUND assert response.error.message == "Method not found" - finally: # pragma: no cover + finally: await read_send_stream.aclose() await write_send_stream.aclose() await read_receive_stream.aclose() @@ -74,7 +74,7 @@ async def test_client_session_known_request_methods_match_server_request_union() try: session = ClientSession(read_stream=read_receive_stream, write_stream=write_send_stream) assert session._known_request_methods == KNOWN_SERVER_REQUEST_METHODS - finally: # pragma: no cover + finally: await read_send_stream.aclose() await write_send_stream.aclose() await read_receive_stream.aclose() @@ -109,7 +109,7 @@ async def test_base_session_known_request_methods_default_to_empty() -> None: assert session._known_request_methods == frozenset() assert session._receive_request_adapter is types.server_request_adapter assert session._receive_notification_adapter is types.server_notification_adapter - finally: # pragma: no cover + finally: await read_send_stream.aclose() await write_send_stream.aclose() await read_receive_stream.aclose() From b87e373b78c9e8212ca145a57abb2399ac37b75a Mon Sep 17 00:00:00 2001 From: Lovish Arora <46993225+lavish0000@users.noreply.github.com> Date: Sun, 8 Mar 2026 10:26:57 +0100 Subject: [PATCH 4/4] test: use context-managed streams in issue 1561 coverage --- tests/issues/test_1561_invalid_method_code.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/tests/issues/test_1561_invalid_method_code.py b/tests/issues/test_1561_invalid_method_code.py index 2f9c08aa8..2f42e0125 100644 --- a/tests/issues/test_1561_invalid_method_code.py +++ b/tests/issues/test_1561_invalid_method_code.py @@ -18,7 +18,7 @@ async def test_invalid_method_returns_method_not_found() -> None: read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10) write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10) - try: + async with read_send_stream, read_receive_stream, write_send_stream, write_receive_stream: async with ServerSession( read_stream=read_receive_stream, write_stream=write_send_stream, @@ -48,11 +48,6 @@ async def test_invalid_method_returns_method_not_found() -> None: assert response.id == 1 assert response.error.code == METHOD_NOT_FOUND assert response.error.message == "Method not found" - finally: - await read_send_stream.aclose() - await write_send_stream.aclose() - await read_receive_stream.aclose() - await write_receive_stream.aclose() class MissingDefaultMethodRequest(BaseModel): @@ -71,14 +66,9 @@ async def test_client_session_known_request_methods_match_server_request_union() read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10) write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10) - try: + async with read_send_stream, read_receive_stream, write_send_stream, write_receive_stream: session = ClientSession(read_stream=read_receive_stream, write_stream=write_send_stream) assert session._known_request_methods == KNOWN_SERVER_REQUEST_METHODS - finally: - await read_send_stream.aclose() - await write_send_stream.aclose() - await read_receive_stream.aclose() - await write_receive_stream.aclose() class DummyBaseSession( @@ -104,13 +94,8 @@ async def test_base_session_known_request_methods_default_to_empty() -> None: read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10) write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10) - try: + async with read_send_stream, read_receive_stream, write_send_stream, write_receive_stream: session = DummyBaseSession(read_stream=read_receive_stream, write_stream=write_send_stream) assert session._known_request_methods == frozenset() assert session._receive_request_adapter is types.server_request_adapter assert session._receive_notification_adapter is types.server_notification_adapter - finally: - await read_send_stream.aclose() - await write_send_stream.aclose() - await read_receive_stream.aclose() - await write_receive_stream.aclose()