Skip to content

Commit 109c391

Browse files
committed
Merge remote-tracking branch 'origin/main' into sep-2468-validate-iss
2 parents 7eeee6c + b7a5bff commit 109c391

23 files changed

Lines changed: 244 additions & 143 deletions

.github/actions/conformance/expected-failures.2026-07-28.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,5 @@ server:
118118
# These scenarios emit no FAILURE checks, only SHOULD-level WARNINGs, but
119119
# the expected-failures evaluator counts WARNINGs as failures. Same entries
120120
# as the draft suite in expected-failures.yml.
121-
# SEP-2164: server returns -32600 (not -32602) and omits error.data.uri.
122-
- sep-2164-resource-not-found
123121
# SEP-2322 SHOULD-level behaviour (re-request missing inputResponses).
124122
- input-required-result-missing-input-response

.github/actions/conformance/expected-failures.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ server:
7171
- http-header-validation
7272
# WARNING-only entries: these scenarios emit no FAILURE checks, only SHOULD-level
7373
# WARNINGs, but the expected-failures evaluator counts WARNINGs as failures.
74-
# SEP-2164: server returns -32600 (not -32602) and omits error.data.uri.
75-
- sep-2164-resource-not-found
7674
# SEP-2322 SHOULD-level behaviour (re-request missing inputResponses).
7775
- input-required-result-missing-input-response
7876
# SEP-2322 negative-case scenarios: input-required-result-validate-input is

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
version: 2
22
updates:
3+
- package-ecosystem: "uv"
4+
directory: "/"
5+
schedule:
6+
interval: monthly
7+
cooldown:
8+
default-days: 14
9+
groups:
10+
python-packages:
11+
patterns:
12+
- "*"
313
- package-ecosystem: "github-actions"
414
directory: "/"
515
schedule:
616
interval: monthly
17+
cooldown:
18+
default-days: 14
719
groups:
820
github-actions:
921
patterns:

.github/workflows/weekly-lockfile-update.yml

Lines changed: 0 additions & 43 deletions
This file was deleted.

docs/migration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Version 2 of the MCP Python SDK introduces several breaking changes to improve t
88

99
## Breaking Changes
1010

11+
### `MCPServer.call_tool()` returns `CallToolResult`
12+
13+
`MCPServer.call_tool()` now always returns a `CallToolResult`. It previously
14+
advertised `Sequence[ContentBlock] | dict[str, Any]` and leaked the internal
15+
conversion shapes (a bare content sequence or a `(content, structured_content)`
16+
tuple), forcing callers to re-assemble a `CallToolResult` themselves.
17+
18+
If you call `MCPServer.call_tool()` directly, read `.content` and
19+
`.structured_content` off the returned `CallToolResult` instead of branching on
20+
the result type.
21+
1122
### `streamablehttp_client` removed
1223

1324
The deprecated `streamablehttp_client` function has been removed. Use `streamable_http_client` instead.
@@ -509,6 +520,12 @@ async def my_tool(x: int, ctx: Context) -> str:
509520

510521
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
511522

523+
### Resource not found returns `-32602` and resource lookups raise typed exceptions (SEP-2164)
524+
525+
Reading a missing resource now returns JSON-RPC error code `-32602` (invalid params) with the requested URI in `error.data` (`{"uri": ...}`), per [SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164). Previously the server returned code `0` with no `data`. Clients can now reliably distinguish not-found from other errors; a template handler that raises `ResourceNotFoundError` (from `mcp.server.mcpserver.exceptions`) produces this same response.
526+
527+
The underlying lookups now raise typed exceptions instead of `ValueError`. `ResourceManager.get_resource()` raises `ResourceNotFoundError` when no resource or template matches the URI, and `ResourceTemplate.create_resource()` raises `ResourceError` when the template function fails. Neither subclasses `ValueError`, so callers catching `ValueError` should switch to `ResourceNotFoundError` / `ResourceError` (both importable from `mcp.server.mcpserver.exceptions`; `ResourceNotFoundError` subclasses `ResourceError`).
528+
512529
### Registering lowlevel handlers from `MCPServer`
513530

514531
`MCPServer` does not expose public APIs for `subscribe_resource`, `unsubscribe_resource`, or `set_logging_level` handlers. In v1, the workaround was to reach into the private lowlevel server and use its decorator methods:
@@ -1232,6 +1249,16 @@ Tasks are expected to return as a separate MCP extension in a future release.
12321249

12331250
## Bug Fixes
12341251

1252+
### OAuth metadata URLs no longer gain a trailing slash
1253+
1254+
`OAuthMetadata`, `ProtectedResourceMetadata`, and `OAuthClientMetadata` now set
1255+
`url_preserve_empty_path=True` (Pydantic 2.12+). A path-less URL parsed from the wire keeps its
1256+
empty path instead of acquiring a trailing slash, so e.g. an `issuer` of `https://as.example.com`
1257+
round-trips as `https://as.example.com` rather than `https://as.example.com/`. This matters for
1258+
RFC 9207 / RFC 8414 issuer comparisons, which require simple string comparison (RFC 3986 §6.2.1).
1259+
URLs constructed in Python from an already-built `AnyHttpUrl` object are unaffected (they were
1260+
normalized at construction); only values parsed from strings/JSON change.
1261+
12351262
### Lowlevel `Server`: `subscribe` capability now correctly reported
12361263

12371264
Previously, the lowlevel `Server` hardcoded `subscribe=False` in resource capabilities even when a `subscribe_resource()` handler was registered. The `subscribe` capability is now dynamically set to `True` when an `on_subscribe_resource` handler is provided. Clients that previously didn't see `subscribe: true` in capabilities will now see it when a handler is registered, which may change client behavior.

src/mcp/client/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ async def main():
9797
9898
Pinning to ``2026-07-28`` or later selects the stateless transport era: no initialize
9999
handshake is sent on the wire (the session synthesizes its `InitializeResult` locally),
100-
and for HTTP the ``MCP-Protocol-Version`` header is set from the first request.
100+
and for HTTP the ``MCP-Protocol-Version`` header is set from the first request. A modern
101+
pin currently requires a URL or `Transport`; the in-memory `Server`/`MCPServer` path
102+
does not yet have a modern entry point.
101103
Leave as ``None`` to negotiate the version via the initialize handshake.
102104
"""
103105

src/mcp/client/streamable_http.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,14 @@ def __init__(self, url: str, protocol_version: str | None = None) -> None:
9393
9494
Args:
9595
url: The endpoint URL.
96-
protocol_version: Pin the MCP-Protocol-Version header from the first request
97-
instead of waiting to snoop it from an InitializeResult. Required for
98-
stateless 2026-07-28 sessions that never send initialize.
96+
protocol_version: Pin the MCP-Protocol-Version header from the first request.
97+
Only honoured for stateless 2026-07-28+ sessions that never send
98+
initialize; for earlier (stateful) versions the header is populated
99+
from the negotiated InitializeResult, so a pre-2026 value is ignored.
99100
"""
100101
self.url = url
101102
self.session_id: str | None = None
102-
self.protocol_version: str | None = protocol_version
103+
self.protocol_version: str | None = protocol_version if protocol_version in MODERN_PROTOCOL_VERSIONS else None
103104

104105
def _per_message_headers(self, message: JSONRPCMessage) -> dict[str, str]:
105106
"""Per-POST routing headers (Mcp-Method, Mcp-Name) for 2026-07-28+ pinned transports.
@@ -158,7 +159,8 @@ def _maybe_extract_session_id_from_response(self, response: httpx.Response) -> N
158159
def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage) -> None:
159160
"""Extract protocol version from initialization response message."""
160161
if self.protocol_version is not None:
161-
# Constructor pin wins over snooping the InitializeResult.
162+
# Only a modern constructor pin reaches here (pre-2026 values are dropped
163+
# in __init__), and a modern pin never sends initialize.
162164
return
163165
if isinstance(message, JSONRPCResponse) and message.result: # pragma: no branch
164166
try:

src/mcp/server/mcpserver/context.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContent
113113
114114
Returns:
115115
The resource content as either text or bytes
116+
117+
Raises:
118+
ResourceNotFoundError: If no resource or template matches the URI.
119+
ResourceError: If template creation or resource reading fails.
116120
"""
117121
assert self._mcp_server is not None, "Context is not available outside of a request"
118122
return await self._mcp_server.read_resource(uri, self)

src/mcp/server/mcpserver/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ class ResourceError(MCPServerError):
1313
"""Error in resource operations."""
1414

1515

16+
class ResourceNotFoundError(ResourceError):
17+
"""Resource does not exist.
18+
19+
Raise this from a resource template handler to signal that the requested instance does not exist;
20+
clients receive `-32602` (invalid params) per
21+
[SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164).
22+
"""
23+
24+
1625
class ToolError(MCPServerError):
1726
"""Error in tool operations."""
1827

src/mcp/server/mcpserver/resources/resource_manager.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from pydantic import AnyUrl
99

10+
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
1011
from mcp.server.mcpserver.resources.base import Resource
1112
from mcp.server.mcpserver.resources.templates import ResourceTemplate
1213
from mcp.server.mcpserver.utilities.logging import get_logger
@@ -79,7 +80,12 @@ def add_template(
7980
return template
8081

8182
async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]) -> Resource:
82-
"""Get resource by URI, checking concrete resources first, then templates."""
83+
"""Get resource by URI, checking concrete resources first, then templates.
84+
85+
Raises:
86+
ResourceNotFoundError: If no resource or template matches the URI.
87+
ResourceError: If a matching template fails to create the resource.
88+
"""
8389
uri_str = str(uri)
8490
logger.debug("Getting resource", extra={"uri": uri_str})
8591

@@ -90,12 +96,9 @@ async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContext
9096
# Then check templates
9197
for template in self._templates.values():
9298
if params := template.matches(uri_str):
93-
try:
94-
return await template.create_resource(uri_str, params, context=context)
95-
except Exception as e: # pragma: no cover
96-
raise ValueError(f"Error creating resource from template: {e}")
99+
return await template.create_resource(uri_str, params, context=context)
97100

98-
raise ValueError(f"Unknown resource: {uri}")
101+
raise ResourceNotFoundError(f"Unknown resource: {uri}")
99102

100103
def list_resources(self) -> list[Resource]:
101104
"""List all registered resources."""

0 commit comments

Comments
 (0)