Skip to content

Commit e18da92

Browse files
authored
Merge branch 'main' into fix/1822-decorator-paramspec
2 parents da2ddd2 + 201de92 commit e18da92

File tree

17 files changed

+254
-62
lines changed

17 files changed

+254
-62
lines changed

examples/servers/simple-auth/mcp_simple_auth/py.typed

Whitespace-only changes.

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ plugins:
126126
group_by_category: false
127127
# 3 because docs are in pages with an H2 just above them
128128
heading_level: 3
129-
import:
129+
inventories:
130130
- url: https://docs.python.org/3/objects.inv
131131
- url: https://docs.pydantic.dev/latest/objects.inv
132132
- url: https://typing-extensions.readthedocs.io/en/latest/objects.inv

src/mcp/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@
6262
ToolUseContent,
6363
UnsubscribeRequest,
6464
)
65-
from .types import (
66-
Role as SamplingRole,
67-
)
65+
from .types import Role as SamplingRole
6866

6967
__all__ = [
7068
"CallToolRequest",

src/mcp/client/session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ def get_server_capabilities(self) -> types.ServerCapabilities | None:
206206
def experimental(self) -> ExperimentalClientFeatures:
207207
"""Experimental APIs for tasks and other features.
208208
209-
WARNING: These APIs are experimental and may change without notice.
209+
!!! warning
210+
These APIs are experimental and may change without notice.
210211
211212
Example:
212213
status = await session.experimental.get_task(task_id)

src/mcp/server/auth/handlers/register.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ async def handle(self, request: Request) -> Response:
7373
),
7474
status_code=400,
7575
)
76-
if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
76+
if "authorization_code" not in client_metadata.grant_types:
7777
return PydanticJSONResponse(
7878
content=RegistrationErrorResponse(
7979
error="invalid_client_metadata",
80-
error_description="grant_types must be authorization_code and refresh_token",
80+
error_description="grant_types must include 'authorization_code'",
8181
),
8282
status_code=400,
8383
)

src/mcp/server/session.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]:
4949
from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures
5050
from mcp.server.models import InitializationOptions
5151
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages
52+
from mcp.shared.exceptions import StatelessModeNotSupported
5253
from mcp.shared.experimental.tasks.capabilities import check_tasks_capability
5354
from mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY
5455
from mcp.shared.message import ServerMessageMetadata, SessionMessage
@@ -93,6 +94,7 @@ def __init__(
9394
stateless: bool = False,
9495
) -> None:
9596
super().__init__(read_stream, write_stream, types.ClientRequest, types.ClientNotification)
97+
self._stateless = stateless
9698
self._initialization_state = (
9799
InitializationState.Initialized if stateless else InitializationState.NotInitialized
98100
)
@@ -311,7 +313,10 @@ async def create_message(
311313
Raises:
312314
McpError: If tools are provided but client doesn't support them.
313315
ValueError: If tool_use or tool_result message structure is invalid.
316+
StatelessModeNotSupported: If called in stateless HTTP mode.
314317
"""
318+
if self._stateless:
319+
raise StatelessModeNotSupported(method="sampling")
315320
client_caps = self._client_params.capabilities if self._client_params else None
316321
validate_sampling_tools(client_caps, tools, tool_choice)
317322
validate_tool_use_result_messages(messages)
@@ -349,6 +354,8 @@ async def create_message(
349354

350355
async def list_roots(self) -> types.ListRootsResult:
351356
"""Send a roots/list request."""
357+
if self._stateless:
358+
raise StatelessModeNotSupported(method="list_roots")
352359
return await self.send_request(
353360
types.ServerRequest(types.ListRootsRequest()),
354361
types.ListRootsResult,
@@ -391,7 +398,12 @@ async def elicit_form(
391398
392399
Returns:
393400
The client's response with form data
401+
402+
Raises:
403+
StatelessModeNotSupported: If called in stateless HTTP mode.
394404
"""
405+
if self._stateless:
406+
raise StatelessModeNotSupported(method="elicitation")
395407
return await self.send_request(
396408
types.ServerRequest(
397409
types.ElicitRequest(
@@ -425,7 +437,12 @@ async def elicit_url(
425437
426438
Returns:
427439
The client's response indicating acceptance, decline, or cancellation
440+
441+
Raises:
442+
StatelessModeNotSupported: If called in stateless HTTP mode.
428443
"""
444+
if self._stateless:
445+
raise StatelessModeNotSupported(method="elicitation")
429446
return await self.send_request(
430447
types.ServerRequest(
431448
types.ElicitRequest(

src/mcp/shared/exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ def __init__(self, error: ErrorData):
1818
self.error = error
1919

2020

21+
class StatelessModeNotSupported(RuntimeError):
22+
"""
23+
Raised when attempting to use a method that is not supported in stateless mode.
24+
25+
Server-to-client requests (sampling, elicitation, list_roots) are not
26+
supported in stateless HTTP mode because there is no persistent connection
27+
for bidirectional communication.
28+
"""
29+
30+
def __init__(self, method: str):
31+
super().__init__(
32+
f"Cannot use {method} in stateless HTTP mode. "
33+
"Stateless mode does not support server-to-client requests. "
34+
"Use stateful mode (stateless_http=False) to enable this feature."
35+
)
36+
self.method = method
37+
38+
2139
class UrlElicitationRequiredError(McpError):
2240
"""
2341
Specialized error for when a tool requires URL mode elicitation(s) before proceeding.

src/mcp/shared/session.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations as _annotations
2+
13
import logging
24
from collections.abc import Callable
35
from contextlib import AsyncExitStack
@@ -72,14 +74,8 @@ def __init__(
7274
request_id: RequestId,
7375
request_meta: RequestParams.Meta | None,
7476
request: ReceiveRequestT,
75-
session: """BaseSession[
76-
SendRequestT,
77-
SendNotificationT,
78-
SendResultT,
79-
ReceiveRequestT,
80-
ReceiveNotificationT
81-
]""",
82-
on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any],
77+
session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
78+
on_complete: Callable[[RequestResponder[ReceiveRequestT, SendResultT]], Any],
8379
message_metadata: MessageMetadata = None,
8480
) -> None:
8581
self.request_id = request_id
@@ -92,7 +88,7 @@ def __init__(
9288
self._on_complete = on_complete
9389
self._entered = False # Track if we're in a context manager
9490

95-
def __enter__(self) -> "RequestResponder[ReceiveRequestT, SendResultT]":
91+
def __enter__(self) -> RequestResponder[ReceiveRequestT, SendResultT]:
9692
"""Enter the context manager, enabling request cancellation tracking."""
9793
self._entered = True
9894
self._cancel_scope = anyio.CancelScope()
@@ -179,7 +175,7 @@ class BaseSession(
179175
_request_id: int
180176
_in_flight: dict[RequestId, RequestResponder[ReceiveRequestT, SendResultT]]
181177
_progress_callbacks: dict[RequestId, ProgressFnT]
182-
_response_routers: list["ResponseRouter"]
178+
_response_routers: list[ResponseRouter]
183179

184180
def __init__(
185181
self,
@@ -210,7 +206,8 @@ def add_response_router(self, router: ResponseRouter) -> None:
210206
response stream mechanism. This is used by TaskResultHandler to route
211207
responses for queued task requests back to their resolvers.
212208
213-
WARNING: This is an experimental API that may change without notice.
209+
!!! warning
210+
This is an experimental API that may change without notice.
214211
215212
Args:
216213
router: A ResponseRouter implementation

src/mcp/types.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations as _annotations
2+
13
from collections.abc import Callable
24
from datetime import datetime
35
from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar
@@ -6,24 +8,6 @@
68
from pydantic.networks import AnyUrl, UrlConstraints
79
from typing_extensions import deprecated
810

9-
"""
10-
Model Context Protocol bindings for Python
11-
12-
These bindings were generated from https://github.com/modelcontextprotocol/specification,
13-
using Claude, with a prompt something like the following:
14-
15-
Generate idiomatic Python bindings for this schema for MCP, or the "Model Context
16-
Protocol." The schema is defined in TypeScript, but there's also a JSON Schema version
17-
for reference.
18-
19-
* For the bindings, let's use Pydantic V2 models.
20-
* Each model should allow extra fields everywhere, by specifying `model_config =
21-
ConfigDict(extra='allow')`. Do this in every case, instead of a custom base class.
22-
* Union types should be represented with a Pydantic `RootModel`.
23-
* Define additional model classes instead of using dictionaries. Do this even if they're
24-
not separate types in the schema.
25-
"""
26-
2711
LATEST_PROTOCOL_VERSION = "2025-11-25"
2812

2913
"""
@@ -557,7 +541,7 @@ class Task(BaseModel):
557541
"""Current task state."""
558542

559543
statusMessage: str | None = None
560-
"""
544+
"""
561545
Optional human-readable message describing the current task state.
562546
This can provide context for any status, including:
563547
- Reasons for "cancelled" status
@@ -1121,7 +1105,7 @@ class ToolResultContent(BaseModel):
11211105
toolUseId: str
11221106
"""The unique identifier that corresponds to the tool call's id field."""
11231107

1124-
content: list["ContentBlock"] = []
1108+
content: list[ContentBlock] = []
11251109
"""
11261110
A list of content objects representing the tool result.
11271111
Defaults to empty list if not provided.
@@ -1523,7 +1507,7 @@ class CreateMessageRequestParams(RequestParams):
15231507
stopSequences: list[str] | None = None
15241508
metadata: dict[str, Any] | None = None
15251509
"""Optional metadata to pass through to the LLM provider."""
1526-
tools: list["Tool"] | None = None
1510+
tools: list[Tool] | None = None
15271511
"""
15281512
Tool definitions for the LLM to use during sampling.
15291513
Requires clientCapabilities.sampling.tools to be present.

tests/issues/test_1027_win_unreachable_cleanup.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,13 @@
1212
import tempfile
1313
import textwrap
1414
from pathlib import Path
15-
from typing import TYPE_CHECKING
1615

1716
import anyio
1817
import pytest
1918

2019
from mcp import ClientSession, StdioServerParameters
2120
from mcp.client.stdio import _create_platform_compatible_process, stdio_client
22-
23-
# TODO(Marcelo): This doesn't seem to be the right path. We should fix this.
24-
if TYPE_CHECKING:
25-
from ..shared.test_win32_utils import escape_path_for_python
26-
else:
27-
from tests.shared.test_win32_utils import escape_path_for_python
21+
from tests.shared.test_win32_utils import escape_path_for_python
2822

2923

3024
@pytest.mark.anyio

0 commit comments

Comments
 (0)