Fix unknown-method error code and add a protocol version registry (#2836)
This commit is contained in:
parent
1e21814ed5
commit
7267818e44
2 changed files with 430 additions and 23 deletions
|
|
@ -168,6 +168,45 @@ async def test_runner_gates_requests_before_initialize(server: SrvT):
|
|||
assert await client.send_raw_request("ping", None) == {}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_runner_unknown_method_before_initialize_raises_method_not_found(server: SrvT):
|
||||
"""An unknown method is METHOD_NOT_FOUND even before initialize: JSON-RPC
|
||||
2.0 reserves -32601 for it, and clients probing a server before the
|
||||
handshake key off that code. The init gate only applies to methods the
|
||||
server actually serves."""
|
||||
async with connected_runner(server, initialized=False) as (client, _):
|
||||
with pytest.raises(MCPError) as exc:
|
||||
await client.send_raw_request("x/unknown", None)
|
||||
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="x/unknown")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_runner_spec_method_without_handler_before_initialize_raises_method_not_found(server: SrvT):
|
||||
"""A spec method the server doesn't serve is METHOD_NOT_FOUND even before
|
||||
initialize: -32601 means "not available on this server", so probing
|
||||
clients get the same answer in every initialization state (the fixture
|
||||
server registers no resources handlers)."""
|
||||
async with connected_runner(server, initialized=False) as (client, _):
|
||||
with pytest.raises(MCPError) as exc:
|
||||
await client.send_raw_request("resources/list", None)
|
||||
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="resources/list")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_runner_custom_method_with_handler_is_still_gated_before_initialize(server: SrvT):
|
||||
"""A custom-registered method is a known method: before initialize it is
|
||||
rejected by the init gate, not answered with METHOD_NOT_FOUND."""
|
||||
|
||||
async def greet(ctx: Ctx, params: RequestParams | None) -> Any:
|
||||
raise NotImplementedError # the gate rejects the request first
|
||||
|
||||
server.add_request_handler("custom/greet", RequestParams, greet)
|
||||
async with connected_runner(server, initialized=False) as (client, _):
|
||||
with pytest.raises(MCPError) as exc:
|
||||
await client.send_raw_request("custom/greet", None)
|
||||
assert exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_runner_routes_to_handler_and_builds_context(server: SrvT):
|
||||
async with connected_runner(server) as (client, runner):
|
||||
|
|
@ -186,7 +225,7 @@ async def test_runner_spec_method_with_no_handler_raises_method_not_found(server
|
|||
async with connected_runner(server) as (client, _):
|
||||
with pytest.raises(MCPError) as exc:
|
||||
await client.send_raw_request("resources/list", None)
|
||||
assert exc.value.error.code == METHOD_NOT_FOUND
|
||||
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="resources/list")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -196,7 +235,7 @@ async def test_runner_non_spec_method_with_no_handler_raises_method_not_found(se
|
|||
async with connected_runner(server) as (client, _):
|
||||
with pytest.raises(MCPError) as exc:
|
||||
await client.send_raw_request("nonexistent/method", None)
|
||||
assert exc.value.error.code == METHOD_NOT_FOUND
|
||||
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="nonexistent/method")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from typing import Any, cast
|
|||
import pytest
|
||||
|
||||
from mcp import types
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from mcp.server.connection import Connection
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.dispatcher import CallOptions
|
||||
|
|
@ -26,6 +27,8 @@ from mcp.types import (
|
|||
SamplingToolsCapability,
|
||||
)
|
||||
|
||||
from .test_runner import connected_runner
|
||||
|
||||
|
||||
class StubDispatcher:
|
||||
"""Records `send_raw_request` / `notify` calls and returns a canned result."""
|
||||
|
|
@ -158,3 +161,64 @@ def test_check_client_capability_delegates_to_connection():
|
|||
session = _make_session(dispatcher, capabilities=ClientCapabilities(sampling=SamplingCapability()))
|
||||
assert session.check_client_capability(ClientCapabilities(sampling=SamplingCapability())) is True
|
||||
assert session.check_client_capability(ClientCapabilities(experimental={"x": {}})) is False
|
||||
|
||||
|
||||
def _runner_server(seen_versions: list[str | None]) -> Server[dict[str, Any]]:
|
||||
"""A lowlevel Server whose tools/list handler records `ctx.session.protocol_version`."""
|
||||
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext[dict[str, Any], Any], params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
seen_versions.append(ctx.session.protocol_version)
|
||||
return types.ListToolsResult(tools=[])
|
||||
|
||||
return Server(name="test-server", version="0.0.1", on_list_tools=list_tools)
|
||||
|
||||
|
||||
def _init_params(protocol_version: str) -> dict[str, Any]:
|
||||
return InitializeRequestParams(
|
||||
protocol_version=protocol_version,
|
||||
capabilities=ClientCapabilities(),
|
||||
client_info=Implementation(name="test-client", version="1.0"),
|
||||
).model_dump(by_alias=True, exclude_none=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_protocol_version_is_none_before_initialize():
|
||||
"""No negotiated version is readable before the initialize handshake."""
|
||||
async with connected_runner(_runner_server([]), initialized=False) as (_client, runner):
|
||||
assert runner.session.protocol_version is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_protocol_version_is_negotiated_version_after_initialize():
|
||||
"""A supported requested version is echoed back and readable on the session,
|
||||
both directly and from inside a handler via `ctx.session`."""
|
||||
seen: list[str | None] = []
|
||||
async with connected_runner(_runner_server(seen), initialized=False) as (client, runner):
|
||||
result = await client.send_raw_request("initialize", _init_params("2025-03-26"))
|
||||
assert result["protocolVersion"] == "2025-03-26"
|
||||
assert runner.session.protocol_version == "2025-03-26"
|
||||
await client.send_raw_request("tools/list", None)
|
||||
assert seen == ["2025-03-26"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_protocol_version_reads_latest_when_requested_version_unsupported():
|
||||
"""An unsupported requested version negotiates down to LATEST_PROTOCOL_VERSION."""
|
||||
async with connected_runner(_runner_server([]), initialized=False) as (client, runner):
|
||||
result = await client.send_raw_request("initialize", _init_params("1999-01-01"))
|
||||
assert result["protocolVersion"] == LATEST_PROTOCOL_VERSION
|
||||
assert runner.session.protocol_version == LATEST_PROTOCOL_VERSION
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_protocol_version_is_none_on_stateless_connection():
|
||||
"""Stateless connections never see a handshake: requests flow, but the
|
||||
negotiated version legitimately stays None."""
|
||||
seen: list[str | None] = []
|
||||
async with connected_runner(_runner_server(seen), initialized=False, stateless=True) as (client, runner):
|
||||
result = await client.send_raw_request("tools/list", None)
|
||||
assert result == {"tools": []}
|
||||
assert seen == [None]
|
||||
assert runner.session.protocol_version is None
|
||||
|
|
|
|||
Loading…
Reference in a new issue