Skip to content

Commit 87e0dbc

Browse files
committed
feat: ServerRunner builds InitializeResult from InitializationOptions
ServerRunner gains init_options (defaults to server.create_initialization_options()). _handle_initialize builds the full InitializeResult from it (name/title/description/version/website_url/icons/ instructions) and negotiates requested-if-in-SUPPORTED_PROTOCOL_VERSIONS- else-LATEST, matching ServerSession._received_request.
1 parent 9bdd153 commit 87e0dbc

2 files changed

Lines changed: 45 additions & 18 deletions

File tree

src/mcp/server/runner.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
from mcp.server.connection import Connection
3030
from mcp.server.context import CallNext, Context, ServerMiddleware
3131
from mcp.server.lowlevel.server import Server
32+
from mcp.server.models import InitializationOptions
3233
from mcp.shared._otel import extract_trace_context, otel_span
3334
from mcp.shared.dispatcher import DispatchContext, Dispatcher, DispatchMiddleware, OnRequest
3435
from mcp.shared.exceptions import MCPError
3536
from mcp.shared.transport_context import TransportContext
37+
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
3638
from mcp.types import (
3739
INVALID_PARAMS,
3840
LATEST_PROTOCOL_VERSION,
@@ -125,6 +127,8 @@ class ServerRunner(Generic[LifespanT]):
125127
dispatcher: Dispatcher[TransportContext]
126128
lifespan_state: LifespanT
127129
has_standalone_channel: bool
130+
init_options: InitializationOptions | None = None
131+
"""`InitializeResult` payload. Defaults to `server.create_initialization_options()`."""
128132
session_id: str | None = None
129133
stateless: bool = False
130134
dispatch_middleware: list[DispatchMiddleware] = field(default_factory=list[DispatchMiddleware])
@@ -134,6 +138,8 @@ class ServerRunner(Generic[LifespanT]):
134138

135139
def __post_init__(self) -> None:
136140
self._initialized = self.stateless
141+
if self.init_options is None:
142+
self.init_options = self.server.create_initialization_options()
137143
self.connection = Connection(
138144
self.dispatcher, has_standalone_channel=self.has_standalone_channel, session_id=self.session_id
139145
)
@@ -234,18 +240,24 @@ def _handle_initialize(self, params: Mapping[str, Any] | None) -> dict[str, Any]
234240
init = InitializeRequestParams.model_validate(params or {})
235241
self.connection.client_info = init.client_info
236242
self.connection.client_capabilities = init.capabilities
237-
# TODO: real version negotiation. This always responds with LATEST,
238-
# which is wrong - the server should pick the highest version both
239-
# sides support and compute a per-connection feature set from it.
240-
# See FOLLOWUPS: "Consolidate per-connection mode/negotiation".
241-
self.connection.protocol_version = (
242-
init.protocol_version if init.protocol_version in {LATEST_PROTOCOL_VERSION} else LATEST_PROTOCOL_VERSION
243-
)
243+
requested = init.protocol_version
244+
negotiated = requested if requested in SUPPORTED_PROTOCOL_VERSIONS else LATEST_PROTOCOL_VERSION
245+
self.connection.protocol_version = negotiated
244246
self._initialized = True
245247
self.connection.initialized.set()
248+
assert self.init_options is not None
249+
opts = self.init_options
246250
result = InitializeResult(
247-
protocol_version=self.connection.protocol_version,
248-
capabilities=self.server.capabilities(),
249-
server_info=Implementation(name=self.server.name, version=self.server.version or "0.0.0"),
251+
protocol_version=negotiated,
252+
capabilities=opts.capabilities,
253+
server_info=Implementation(
254+
name=opts.server_name,
255+
title=opts.title,
256+
description=opts.description,
257+
version=opts.server_version,
258+
website_url=opts.website_url,
259+
icons=opts.icons,
260+
),
261+
instructions=opts.instructions,
250262
)
251263
return _dump_result(result)

tests/server/test_runner.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
from mcp.server.connection import Connection
1919
from mcp.server.context import Context
2020
from mcp.server.lowlevel.server import NotificationOptions, Server
21+
from mcp.server.models import InitializationOptions
2122
from mcp.server.runner import ServerRunner, otel_middleware
2223
from mcp.shared.direct_dispatcher import DirectDispatcher, create_direct_dispatcher_pair
2324
from mcp.shared.dispatcher import DispatchMiddleware
2425
from mcp.shared.exceptions import MCPError
26+
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
2527
from mcp.types import (
2628
INTERNAL_ERROR,
2729
INVALID_PARAMS,
@@ -77,6 +79,7 @@ async def connected_runner(
7779
initialized: bool = True,
7880
stateless: bool = False,
7981
has_standalone_channel: bool = True,
82+
init_options: InitializationOptions | None = None,
8083
session_id: str | None = None,
8184
headers: Mapping[str, str] | None = None,
8285
dispatch_middleware: list[DispatchMiddleware] | None = None,
@@ -94,6 +97,7 @@ async def connected_runner(
9497
dispatcher=server_d,
9598
lifespan_state={},
9699
has_standalone_channel=has_standalone_channel,
100+
init_options=init_options,
97101
session_id=session_id,
98102
stateless=stateless,
99103
dispatch_middleware=dispatch_middleware or [],
@@ -327,20 +331,31 @@ async def greet(ctx: Any, params: GreetParams) -> dict[str, Any]:
327331

328332

329333
@pytest.mark.anyio
330-
async def test_server_capabilities_reflects_ctor_options_in_initialize_result():
334+
async def test_runner_initialize_result_reflects_init_options():
331335
async def list_tools(ctx: Any, params: PaginatedRequestParams | None) -> ListToolsResult:
332336
raise NotImplementedError
333337

334-
server: SrvT = Server(
335-
name="caps-test",
336-
on_list_tools=list_tools,
337-
notification_options=NotificationOptions(tools_changed=True),
338-
experimental_capabilities={"ext": {"k": "v"}},
339-
)
340-
async with connected_runner(server, initialized=False) as (client, _):
338+
server: SrvT = Server(name="caps-test", on_list_tools=list_tools, instructions="be nice")
339+
init_options = server.create_initialization_options(NotificationOptions(tools_changed=True), {"ext": {"k": "v"}})
340+
async with connected_runner(server, initialized=False, init_options=init_options) as (client, _):
341341
result = await client.send_raw_request("initialize", _initialize_params())
342342
assert result["capabilities"]["tools"]["listChanged"] is True
343343
assert result["capabilities"]["experimental"] == {"ext": {"k": "v"}}
344+
assert result["serverInfo"]["name"] == "caps-test"
345+
assert result["instructions"] == "be nice"
346+
347+
348+
@pytest.mark.anyio
349+
async def test_runner_initialize_echoes_supported_version_and_falls_back_to_latest(server: SrvT):
350+
oldest = SUPPORTED_PROTOCOL_VERSIONS[0]
351+
async with connected_runner(server, initialized=False) as (client, _):
352+
params = {**_initialize_params(), "protocolVersion": oldest}
353+
result = await client.send_raw_request("initialize", params)
354+
assert result["protocolVersion"] == oldest
355+
async with connected_runner(server, initialized=False) as (client, _):
356+
params = {**_initialize_params(), "protocolVersion": "1999-01-01"}
357+
result = await client.send_raw_request("initialize", params)
358+
assert result["protocolVersion"] == LATEST_PROTOCOL_VERSION
344359

345360

346361
@pytest.mark.anyio

0 commit comments

Comments
 (0)