Add meta to Client methods (#1923)

This commit is contained in:
Marcelo Trylesinski 2026-01-22 14:50:39 +01:00 • committed by GitHub
commit f0ab53e194
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 221 additions and 156 deletions

View file

@ -2119,8 +2119,6 @@ uv run client
import asyncio import asyncio
import os import os
from pydantic import AnyUrl
from mcp import ClientSession, StdioServerParameters, types from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client from mcp.client.stdio import stdio_client
from mcp.shared.context import RequestContext from mcp.shared.context import RequestContext
@ -2173,7 +2171,7 @@ async def run():
print(f"Available tools: {[t.name for t in tools.tools]}") print(f"Available tools: {[t.name for t in tools.tools]}")
# Read a resource (greeting resource from fastmcp_quickstart) # Read a resource (greeting resource from fastmcp_quickstart)
resource_content = await session.read_resource(AnyUrl("greeting://World")) resource_content = await session.read_resource("greeting://World")
content_block = resource_content.contents[0] content_block = resource_content.contents[0]
if isinstance(content_block, types.TextContent): if isinstance(content_block, types.TextContent):
print(f"Resource content: {content_block.text}") print(f"Resource content: {content_block.text}")

View file

@ -233,6 +233,35 @@ notification = server_notification_adapter.validate_python(data)
All adapters are exported from `mcp.types`. All adapters are exported from `mcp.types`.
### `RequestParams.Meta` replaced with `RequestParamsMeta` TypedDict
The nested `RequestParams.Meta` Pydantic model class has been replaced with a top-level `RequestParamsMeta` TypedDict. This affects the `ctx.meta` field in request handlers and any code that imports or references this type.
**Key changes:**
- `RequestParams.Meta` (Pydantic model) → `RequestParamsMeta` (TypedDict)
- Attribute access (`meta.progress_token`) → Dictionary access (`meta.get("progress_token")`)
- `progress_token` field changed from `ProgressToken | None = None` to `NotRequired[ProgressToken]`
`
**In request context handlers:**
```python
# Before (v1)
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
ctx = server.request_context
if ctx.meta and ctx.meta.progress_token:
await ctx.session.send_progress_notification(ctx.meta.progress_token, 0.5, 100)
# After (v2)
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
ctx = server.request_context
if ctx.meta and "progress_token" in ctx.meta:
await ctx.session.send_progress_notification(ctx.meta["progress_token"], 0.5, 100)
```
### Resource URI type changed from `AnyUrl` to `str` ### Resource URI type changed from `AnyUrl` to `str`
The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected. The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.
@ -274,7 +303,19 @@ Affected types:
- `UnsubscribeRequestParams.uri` - `UnsubscribeRequestParams.uri`
- `ResourceUpdatedNotificationParams.uri` - `ResourceUpdatedNotificationParams.uri`
The `ClientSession.read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` methods now accept both `str` and `AnyUrl` for backwards compatibility. The `Client` and `ClientSession` methods `read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` now only accept `str` for the `uri` parameter. If you were passing `AnyUrl` objects, convert them to strings:
```python
# Before (v1)
from pydantic import AnyUrl
await client.read_resource(AnyUrl("test://resource"))
# After (v2)
await client.read_resource("test://resource")
# Or if you have an AnyUrl from elsewhere:
await client.read_resource(str(my_any_url))
```
## Deprecations ## Deprecations

View file

@ -161,7 +161,9 @@ async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100") await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
# Return progress token as string # Return progress token as string
progress_token = ctx.request_context.meta.progress_token if ctx.request_context and ctx.request_context.meta else 0 progress_token = (
ctx.request_context.meta.get("progress_token") if ctx.request_context and ctx.request_context.meta else 0
)
return str(progress_token) return str(progress_token)

View file

@ -5,8 +5,6 @@ uv run client
import asyncio import asyncio
import os import os
from pydantic import AnyUrl
from mcp import ClientSession, StdioServerParameters, types from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client from mcp.client.stdio import stdio_client
from mcp.shared.context import RequestContext from mcp.shared.context import RequestContext
@ -59,7 +57,7 @@ async def run():
print(f"Available tools: {[t.name for t in tools.tools]}") print(f"Available tools: {[t.name for t in tools.tools]}")
# Read a resource (greeting resource from fastmcp_quickstart) # Read a resource (greeting resource from fastmcp_quickstart)
resource_content = await session.read_resource(AnyUrl("greeting://World")) resource_content = await session.read_resource("greeting://World")
content_block = resource_content.contents[0] content_block = resource_content.contents[0]
if isinstance(content_block, types.TextContent): if isinstance(content_block, types.TextContent):
print(f"Resource content: {content_block.text}") print(f"Resource content: {content_block.text}")

View file

@ -37,7 +37,7 @@ dependencies = [
"jsonschema>=4.20.0", "jsonschema>=4.20.0",
"pywin32>=311; sys_platform == 'win32'", "pywin32>=311; sys_platform == 'win32'",
"pyjwt[crypto]>=2.10.1", "pyjwt[crypto]>=2.10.1",
"typing-extensions>=4.9.0", "typing-extensions>=4.13.0",
"typing-inspection>=0.4.1", "typing-inspection>=0.4.1",
] ]

View file

@ -5,21 +5,29 @@ from __future__ import annotations
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from typing import Any from typing import Any
from pydantic import AnyUrl
import mcp.types as types
from mcp.client._memory import InMemoryTransport from mcp.client._memory import InMemoryTransport
from mcp.client.session import ( from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
ClientSession,
ElicitationFnT,
ListRootsFnT,
LoggingFnT,
MessageHandlerFnT,
SamplingFnT,
)
from mcp.server import Server from mcp.server import Server
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from mcp.shared.session import ProgressFnT from mcp.shared.session import ProgressFnT
from mcp.types import (
CallToolResult,
CompleteResult,
EmptyResult,
GetPromptResult,
Implementation,
ListPromptsResult,
ListResourcesResult,
ListResourceTemplatesResult,
ListToolsResult,
LoggingLevel,
PaginatedRequestParams,
PromptReference,
ReadResourceResult,
RequestParamsMeta,
ResourceTemplateReference,
ServerCapabilities,
)
class Client: class Client:
@ -39,8 +47,11 @@ class Client:
def add(a: int, b: int) -> int: def add(a: int, b: int) -> int:
return a + b return a + b
async with Client(server) as client: async def main():
result = await client.call_tool("add", {"a": 1, "b": 2}) async with Client(server) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
asyncio.run(main())
``` ```
""" """
@ -62,7 +73,7 @@ class Client:
list_roots_callback: ListRootsFnT | None = None, list_roots_callback: ListRootsFnT | None = None,
logging_callback: LoggingFnT | None = None, logging_callback: LoggingFnT | None = None,
message_handler: MessageHandlerFnT | None = None, message_handler: MessageHandlerFnT | None = None,
client_info: types.Implementation | None = None, client_info: Implementation | None = None,
elicitation_callback: ElicitationFnT | None = None, elicitation_callback: ElicitationFnT | None = None,
) -> None: ) -> None:
"""Initialize the client with a server. """Initialize the client with a server.
@ -143,13 +154,13 @@ class Client:
return self._session return self._session
@property @property
def server_capabilities(self) -> types.ServerCapabilities | None: def server_capabilities(self) -> ServerCapabilities | None:
"""The server capabilities received during initialization, or None if not yet initialized.""" """The server capabilities received during initialization, or None if not yet initialized."""
return self.session.get_server_capabilities() return self.session.get_server_capabilities()
async def send_ping(self) -> types.EmptyResult: async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
"""Send a ping request to the server.""" """Send a ping request to the server."""
return await self.session.send_ping() return await self.session.send_ping(meta=meta)
async def send_progress_notification( async def send_progress_notification(
self, self,
@ -166,36 +177,47 @@ class Client:
message=message, message=message,
) )
async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult: async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
"""Set the logging level on the server.""" """Set the logging level on the server."""
return await self.session.set_logging_level(level) return await self.session.set_logging_level(level=level, meta=meta)
async def list_resources(self, *, cursor: str | None = None) -> types.ListResourcesResult: async def list_resources(
self,
*,
cursor: str | None = None,
meta: RequestParamsMeta | None = None,
) -> ListResourcesResult:
"""List available resources from the server.""" """List available resources from the server."""
return await self.session.list_resources(params=types.PaginatedRequestParams(cursor=cursor)) return await self.session.list_resources(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
async def list_resource_templates(self, *, cursor: str | None = None) -> types.ListResourceTemplatesResult: async def list_resource_templates(
self,
*,
cursor: str | None = None,
meta: RequestParamsMeta | None = None,
) -> ListResourceTemplatesResult:
"""List available resource templates from the server.""" """List available resource templates from the server."""
return await self.session.list_resource_templates(params=types.PaginatedRequestParams(cursor=cursor)) return await self.session.list_resource_templates(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult: async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> ReadResourceResult:
"""Read a resource from the server. """Read a resource from the server.
Args: Args:
uri: The URI of the resource to read. uri: The URI of the resource to read.
meta: Additional metadata for the request
Returns: Returns:
The resource content. The resource content.
""" """
return await self.session.read_resource(uri) return await self.session.read_resource(uri, meta=meta)
async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult: async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
"""Subscribe to resource updates.""" """Subscribe to resource updates."""
return await self.session.subscribe_resource(uri) return await self.session.subscribe_resource(uri, meta=meta)
async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult: async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
"""Unsubscribe from resource updates.""" """Unsubscribe from resource updates."""
return await self.session.unsubscribe_resource(uri) return await self.session.unsubscribe_resource(uri, meta=meta)
async def call_tool( async def call_tool(
self, self,
@ -204,8 +226,8 @@ class Client:
read_timeout_seconds: float | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
*, *,
meta: dict[str, Any] | None = None, meta: RequestParamsMeta | None = None,
) -> types.CallToolResult: ) -> CallToolResult:
"""Call a tool on the server. """Call a tool on the server.
Args: Args:
@ -226,28 +248,36 @@ class Client:
meta=meta, meta=meta,
) )
async def list_prompts(self, *, cursor: str | None = None) -> types.ListPromptsResult: async def list_prompts(
self,
*,
cursor: str | None = None,
meta: RequestParamsMeta | None = None,
) -> ListPromptsResult:
"""List available prompts from the server.""" """List available prompts from the server."""
return await self.session.list_prompts(params=types.PaginatedRequestParams(cursor=cursor)) return await self.session.list_prompts(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult: async def get_prompt(
self, name: str, arguments: dict[str, str] | None = None, *, meta: RequestParamsMeta | None = None
) -> GetPromptResult:
"""Get a prompt from the server. """Get a prompt from the server.
Args: Args:
name: The name of the prompt name: The name of the prompt
arguments: Arguments to pass to the prompt arguments: Arguments to pass to the prompt
meta: Additional metadata for the request
Returns: Returns:
The prompt content. The prompt content.
""" """
return await self.session.get_prompt(name=name, arguments=arguments) return await self.session.get_prompt(name=name, arguments=arguments, meta=meta)
async def complete( async def complete(
self, self,
ref: types.ResourceTemplateReference | types.PromptReference, ref: ResourceTemplateReference | PromptReference,
argument: dict[str, str], argument: dict[str, str],
context_arguments: dict[str, str] | None = None, context_arguments: dict[str, str] | None = None,
) -> types.CompleteResult: ) -> CompleteResult:
"""Get completions for a prompt or resource template argument. """Get completions for a prompt or resource template argument.
Args: Args:
@ -260,9 +290,9 @@ class Client:
""" """
return await self.session.complete(ref=ref, argument=argument, context_arguments=context_arguments) return await self.session.complete(ref=ref, argument=argument, context_arguments=context_arguments)
async def list_tools(self, *, cursor: str | None = None) -> types.ListToolsResult: async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta | None = None) -> ListToolsResult:
"""List available tools from the server.""" """List available tools from the server."""
return await self.session.list_tools(params=types.PaginatedRequestParams(cursor=cursor)) return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
async def send_roots_list_changed(self) -> None: async def send_roots_list_changed(self) -> None:
"""Send a notification that the roots list has changed.""" """Send a notification that the roots list has changed."""

View file

@ -28,6 +28,7 @@ from typing import TYPE_CHECKING, Any, TypeVar
import mcp.types as types import mcp.types as types
from mcp.shared.experimental.tasks.polling import poll_until_terminal from mcp.shared.experimental.tasks.polling import poll_until_terminal
from mcp.types._types import RequestParamsMeta
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.client.session import ClientSession from mcp.client.session import ClientSession
@ -53,7 +54,7 @@ class ExperimentalClientFeatures:
arguments: dict[str, Any] | None = None, arguments: dict[str, Any] | None = None,
*, *,
ttl: int = 60000, ttl: int = 60000,
meta: dict[str, Any] | None = None, meta: RequestParamsMeta | None = None,
) -> types.CreateTaskResult: ) -> types.CreateTaskResult:
"""Call a tool as a task, returning a CreateTaskResult for polling. """Call a tool as a task, returning a CreateTaskResult for polling.
@ -87,17 +88,13 @@ class ExperimentalClientFeatures:
# Get result # Get result
final = await session.experimental.get_task_result(task_id, CallToolResult) final = await session.experimental.get_task_result(task_id, CallToolResult)
""" """
_meta: types.RequestParams.Meta | None = None
if meta is not None:
_meta = types.RequestParams.Meta(**meta)
return await self._session.send_request( return await self._session.send_request(
types.CallToolRequest( types.CallToolRequest(
params=types.CallToolRequestParams( params=types.CallToolRequestParams(
name=name, name=name,
arguments=arguments, arguments=arguments,
task=types.TaskMetadata(ttl=ttl), task=types.TaskMetadata(ttl=ttl),
_meta=_meta, _meta=meta,
), ),
), ),
types.CreateTaskResult, types.CreateTaskResult,
@ -113,9 +110,7 @@ class ExperimentalClientFeatures:
GetTaskResult containing the task status and metadata GetTaskResult containing the task status and metadata
""" """
return await self._session.send_request( return await self._session.send_request(
types.GetTaskRequest( types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)),
params=types.GetTaskRequestParams(task_id=task_id),
),
types.GetTaskResult, types.GetTaskResult,
) )

View file

@ -3,7 +3,7 @@ from typing import Any, Protocol
import anyio.lowlevel import anyio.lowlevel
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from pydantic import AnyUrl, TypeAdapter from pydantic import TypeAdapter
import mcp.types as types import mcp.types as types
from mcp.client.experimental import ExperimentalClientFeatures from mcp.client.experimental import ExperimentalClientFeatures
@ -12,6 +12,7 @@ from mcp.shared.context import RequestContext
from mcp.shared.message import SessionMessage from mcp.shared.message import SessionMessage
from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS 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") DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
@ -216,9 +217,9 @@ class ClientSession(
self._experimental_features = ExperimentalClientFeatures(self) self._experimental_features = ExperimentalClientFeatures(self)
return self._experimental_features return self._experimental_features
async def send_ping(self) -> types.EmptyResult: async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
"""Send a ping request.""" """Send a ping request."""
return await self.send_request(types.PingRequest(), types.EmptyResult) return await self.send_request(types.PingRequest(params=types.RequestParams(_meta=meta)), types.EmptyResult)
async def send_progress_notification( async def send_progress_notification(
self, self,
@ -226,6 +227,8 @@ class ClientSession(
progress: float, progress: float,
total: float | None = None, total: float | None = None,
message: str | None = None, message: str | None = None,
*,
meta: RequestParamsMeta | None = None,
) -> None: ) -> None:
"""Send a progress notification.""" """Send a progress notification."""
await self.send_notification( await self.send_notification(
@ -235,14 +238,20 @@ class ClientSession(
progress=progress, progress=progress,
total=total, total=total,
message=message, message=message,
_meta=meta,
), ),
) )
) )
async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult: async def set_logging_level(
self,
level: types.LoggingLevel,
*,
meta: RequestParamsMeta | None = None,
) -> types.EmptyResult:
"""Send a logging/setLevel request.""" """Send a logging/setLevel request."""
return await self.send_request( # pragma: no cover return await self.send_request( # pragma: no cover
types.SetLevelRequest(params=types.SetLevelRequestParams(level=level)), types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)),
types.EmptyResult, types.EmptyResult,
) )
@ -267,24 +276,24 @@ class ClientSession(
types.ListResourceTemplatesResult, types.ListResourceTemplatesResult,
) )
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult: async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.ReadResourceResult:
"""Send a resources/read request.""" """Send a resources/read request."""
return await self.send_request( return await self.send_request(
types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=str(uri))), types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=uri, _meta=meta)),
types.ReadResourceResult, types.ReadResourceResult,
) )
async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult: async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
"""Send a resources/subscribe request.""" """Send a resources/subscribe request."""
return await self.send_request( # pragma: no cover return await self.send_request( # pragma: no cover
types.SubscribeRequest(params=types.SubscribeRequestParams(uri=str(uri))), types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)),
types.EmptyResult, types.EmptyResult,
) )
async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult: async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
"""Send a resources/unsubscribe request.""" """Send a resources/unsubscribe request."""
return await self.send_request( # pragma: no cover return await self.send_request( # pragma: no cover
types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=str(uri))), types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)),
types.EmptyResult, types.EmptyResult,
) )
@ -295,17 +304,13 @@ class ClientSession(
read_timeout_seconds: float | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
*, *,
meta: dict[str, Any] | None = None, meta: RequestParamsMeta | None = None,
) -> types.CallToolResult: ) -> types.CallToolResult:
"""Send a tools/call request with optional progress callback support.""" """Send a tools/call request with optional progress callback support."""
_meta: types.RequestParams.Meta | None = None
if meta is not None:
_meta = types.RequestParams.Meta(**meta)
result = await self.send_request( result = await self.send_request(
types.CallToolRequest( types.CallToolRequest(
params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=_meta), params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=meta),
), ),
types.CallToolResult, types.CallToolResult,
request_read_timeout_seconds=read_timeout_seconds, request_read_timeout_seconds=read_timeout_seconds,
@ -351,11 +356,17 @@ class ClientSession(
""" """
return await self.send_request(types.ListPromptsRequest(params=params), types.ListPromptsResult) return await self.send_request(types.ListPromptsRequest(params=params), types.ListPromptsResult)
async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult: async def get_prompt(
self,
name: str,
arguments: dict[str, str] | None = None,
*,
meta: RequestParamsMeta | None = None,
) -> types.GetPromptResult:
"""Send a prompts/get request.""" """Send a prompts/get request."""
return await self.send_request( return await self.send_request(
types.GetPromptRequest( types.GetPromptRequest(
params=types.GetPromptRequestParams(name=name, arguments=arguments), params=types.GetPromptRequestParams(name=name, arguments=arguments, _meta=meta),
), ),
types.GetPromptResult, types.GetPromptResult,
) )

View file

@ -196,7 +196,7 @@ class ClientSessionGroup:
read_timeout_seconds: float | None = None, read_timeout_seconds: float | None = None,
progress_callback: ProgressFnT | None = None, progress_callback: ProgressFnT | None = None,
*, *,
meta: dict[str, Any] | None = None, meta: types.RequestParamsMeta | None = None,
) -> types.CallToolResult: ) -> types.CallToolResult:
"""Executes a tool given its name and arguments.""" """Executes a tool given its name and arguments."""
session = self._tool_to_session[name] session = self._tool_to_session[name]

View file

@ -1049,7 +1049,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
total: Optional total value e.g. 100 total: Optional total value e.g. 100
message: Optional message e.g. Starting render... message: Optional message e.g. Starting render...
""" """
progress_token = self.request_context.meta.progress_token if self.request_context.meta else None progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
if progress_token is None: # pragma: no cover if progress_token is None: # pragma: no cover
return return
@ -1174,9 +1174,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
@property @property
def client_id(self) -> str | None: def client_id(self) -> str | None:
"""Get the client ID if available.""" """Get the client ID if available."""
return ( return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
) # pragma: no cover
@property @property
def request_id(self) -> str: def request_id(self) -> str:

View file

@ -7,7 +7,7 @@ from typing_extensions import TypeVar
from mcp.shared.message import CloseSSEStreamCallback from mcp.shared.message import CloseSSEStreamCallback
from mcp.shared.session import BaseSession from mcp.shared.session import BaseSession
from mcp.types import RequestId, RequestParams from mcp.types import RequestId, RequestParamsMeta
SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any]) SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any])
LifespanContextT = TypeVar("LifespanContextT") LifespanContextT = TypeVar("LifespanContextT")
@ -17,7 +17,7 @@ RequestT = TypeVar("RequestT", default=Any)
@dataclass @dataclass
class RequestContext(Generic[SessionT, LifespanContextT, RequestT]): class RequestContext(Generic[SessionT, LifespanContextT, RequestT]):
request_id: RequestId request_id: RequestId
meta: RequestParams.Meta | None meta: RequestParamsMeta | None
session: SessionT session: SessionT
lifespan_context: LifespanContextT lifespan_context: LifespanContextT
# NOTE: This is typed as Any to avoid circular imports. The actual type is # NOTE: This is typed as Any to avoid circular imports. The actual type is

View file

@ -48,10 +48,11 @@ def progress(
ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT], ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
None, None,
]: ]:
if ctx.meta is None or ctx.meta.progress_token is None: # pragma: no cover progress_token = ctx.meta.get("progress_token") if ctx.meta else None
if progress_token is None: # pragma: no cover
raise ValueError("No progress token provided") raise ValueError("No progress token provided")
progress_ctx = ProgressContext(ctx.session, ctx.meta.progress_token, total) progress_ctx = ProgressContext(ctx.session, progress_token, total)
try: try:
yield progress_ctx yield progress_ctx
finally: finally:

View file

@ -28,7 +28,8 @@ from mcp.types import (
JSONRPCRequest, JSONRPCRequest,
JSONRPCResponse, JSONRPCResponse,
ProgressNotification, ProgressNotification,
RequestParams, ProgressToken,
RequestParamsMeta,
ServerNotification, ServerNotification,
ServerRequest, ServerRequest,
ServerResult, ServerResult,
@ -71,7 +72,7 @@ class RequestResponder(Generic[ReceiveRequestT, SendResultT]):
def __init__( def __init__(
self, self,
request_id: RequestId, request_id: RequestId,
request_meta: RequestParams.Meta | None, request_meta: RequestParamsMeta | None,
request: ReceiveRequestT, request: ReceiveRequestT,
session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT], session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
on_complete: Callable[[RequestResponder[ReceiveRequestT, SendResultT]], Any], on_complete: Callable[[RequestResponder[ReceiveRequestT, SendResultT]], Any],
@ -501,7 +502,7 @@ class BaseSession(
async def send_progress_notification( async def send_progress_notification(
self, self,
progress_token: str | int, progress_token: ProgressToken,
progress: float, progress: float,
total: float | None = None, total: float | None = None,
message: str | None = None, message: str | None = None,

View file

@ -90,7 +90,6 @@ from mcp.types._types import (
LoggingLevel, LoggingLevel,
LoggingMessageNotification, LoggingMessageNotification,
LoggingMessageNotificationParams, LoggingMessageNotificationParams,
MCPModel,
MethodT, MethodT,
ModelHint, ModelHint,
ModelPreferences, ModelPreferences,
@ -116,6 +115,7 @@ from mcp.types._types import (
RelatedTaskMetadata, RelatedTaskMetadata,
Request, Request,
RequestParams, RequestParams,
RequestParamsMeta,
RequestParamsT, RequestParamsT,
Resource, Resource,
ResourceContents, ResourceContents,
@ -235,12 +235,12 @@ __all__ = [
"TaskExecutionMode", "TaskExecutionMode",
"TaskStatus", "TaskStatus",
# Base classes # Base classes
"MCPModel",
"BaseMetadata", "BaseMetadata",
"Request", "Request",
"Notification", "Notification",
"Result", "Result",
"RequestParams", "RequestParams",
"RequestParamsMeta",
"NotificationParams", "NotificationParams",
"PaginatedRequest", "PaginatedRequest",
"PaginatedRequestParams", "PaginatedRequestParams",

View file

@ -5,6 +5,7 @@ from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar
from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
from typing_extensions import NotRequired, TypedDict
from mcp.types.jsonrpc import RequestId from mcp.types.jsonrpc import RequestId
@ -35,6 +36,19 @@ class MCPModel(BaseModel):
model_config = ConfigDict(extra="allow", alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(extra="allow", alias_generator=to_camel, populate_by_name=True)
Meta: TypeAlias = dict[str, Any]
class RequestParamsMeta(TypedDict, extra_items=Any):
progress_token: NotRequired[ProgressToken]
"""
If specified, the caller requests out-of-band progress notifications for
this request (as represented by notifications/progress). The value of this
parameter is an opaque token that will be attached to any subsequent
notifications. The receiver is not obligated to provide these notifications.
"""
class TaskMetadata(MCPModel): class TaskMetadata(MCPModel):
"""Metadata for augmenting a request with task execution. """Metadata for augmenting a request with task execution.
Include this in the `task` field of the request parameters. Include this in the `task` field of the request parameters.
@ -45,15 +59,6 @@ class TaskMetadata(MCPModel):
class RequestParams(MCPModel): class RequestParams(MCPModel):
class Meta(MCPModel):
progress_token: ProgressToken | None = None
"""
If specified, the caller requests out-of-band progress notifications for
this request (as represented by notifications/progress). The value of this
parameter is an opaque token that will be attached to any subsequent
notifications. The receiver is not obligated to provide these notifications.
"""
task: TaskMetadata | None = None task: TaskMetadata | None = None
""" """
If specified, the caller is requesting task-augmented execution for this request. If specified, the caller is requesting task-augmented execution for this request.
@ -64,7 +69,7 @@ class RequestParams(MCPModel):
for task augmentation of specific request types in their capabilities. for task augmentation of specific request types in their capabilities.
""" """
meta: Meta | None = Field(alias="_meta", default=None) meta: RequestParamsMeta | None = Field(alias="_meta", default=None)
class PaginatedRequestParams(RequestParams): class PaginatedRequestParams(RequestParams):
@ -76,9 +81,6 @@ class PaginatedRequestParams(RequestParams):
class NotificationParams(MCPModel): class NotificationParams(MCPModel):
class Meta(MCPModel):
pass
meta: Meta | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
@ -115,7 +117,7 @@ class Notification(MCPModel, Generic[NotificationParamsT, MethodT]):
class Result(MCPModel): class Result(MCPModel):
"""Base class for JSON-RPC results.""" """Base class for JSON-RPC results."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -624,7 +626,7 @@ class Resource(BaseMetadata):
icons: list[Icon] | None = None icons: list[Icon] | None = None
"""An optional list of icons for this resource.""" """An optional list of icons for this resource."""
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -649,7 +651,7 @@ class ResourceTemplate(BaseMetadata):
icons: list[Icon] | None = None icons: list[Icon] | None = None
"""An optional list of icons for this resource template.""" """An optional list of icons for this resource template."""
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -698,7 +700,7 @@ class ResourceContents(MCPModel):
"""The URI of this resource.""" """The URI of this resource."""
mime_type: str | None = None mime_type: str | None = None
"""The MIME type of this resource, if known.""" """The MIME type of this resource, if known."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -821,7 +823,7 @@ class Prompt(BaseMetadata):
"""A list of arguments to use for templating the prompt.""" """A list of arguments to use for templating the prompt."""
icons: list[Icon] | None = None icons: list[Icon] | None = None
"""An optional list of icons for this prompt.""" """An optional list of icons for this prompt."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -857,7 +859,7 @@ class TextContent(MCPModel):
text: str text: str
"""The text content of the message.""" """The text content of the message."""
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -876,7 +878,7 @@ class ImageContent(MCPModel):
image types. image types.
""" """
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -895,7 +897,7 @@ class AudioContent(MCPModel):
audio types. audio types.
""" """
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -922,7 +924,7 @@ class ToolUseContent(MCPModel):
input: dict[str, Any] input: dict[str, Any]
"""Arguments to pass to the tool. Must conform to the tool's inputSchema.""" """Arguments to pass to the tool. Must conform to the tool's inputSchema."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -956,7 +958,7 @@ class ToolResultContent(MCPModel):
is_error: bool | None = None is_error: bool | None = None
"""Whether the tool execution resulted in an error.""" """Whether the tool execution resulted in an error."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -980,7 +982,7 @@ class SamplingMessage(MCPModel):
Message content. Can be a single content block or an array of content blocks Message content. Can be a single content block or an array of content blocks
for multi-modal messages and tool interactions. for multi-modal messages and tool interactions.
""" """
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -1003,7 +1005,7 @@ class EmbeddedResource(MCPModel):
type: Literal["resource"] = "resource" type: Literal["resource"] = "resource"
resource: TextResourceContents | BlobResourceContents resource: TextResourceContents | BlobResourceContents
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -1134,7 +1136,7 @@ class Tool(BaseMetadata):
"""An optional list of icons for this tool.""" """An optional list of icons for this tool."""
annotations: ToolAnnotations | None = None annotations: ToolAnnotations | None = None
"""Optional additional tool information.""" """Optional additional tool information."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.
@ -1482,7 +1484,7 @@ class Root(MCPModel):
identifier for the root, which may be useful for display purposes or for identifier for the root, which may be useful for display purposes or for
referencing the root in other parts of the application. referencing the root in other parts of the application.
""" """
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: Meta | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage. for notes on _meta usage.

View file

@ -19,6 +19,7 @@ from mcp.types import (
JSONRPCNotification, JSONRPCNotification,
JSONRPCRequest, JSONRPCRequest,
JSONRPCResponse, JSONRPCResponse,
RequestParamsMeta,
ServerCapabilities, ServerCapabilities,
TextContent, TextContent,
client_notification_adapter, client_notification_adapter,
@ -608,7 +609,7 @@ async def test_get_server_capabilities():
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.parametrize(argnames="meta", argvalues=[None, {"toolMeta": "value"}]) @pytest.mark.parametrize(argnames="meta", argvalues=[None, {"toolMeta": "value"}])
async def test_client_tool_call_with_meta(meta: dict[str, Any] | None): async def test_client_tool_call_with_meta(meta: RequestParamsMeta | None):
"""Test that client tool call requests can include metadata""" """Test that client tool call requests can include metadata"""
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1) client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1) server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)

View file

@ -78,8 +78,8 @@ async def test_run_task_basic_flow() -> None:
ctx.experimental.validate_task_mode(TASK_REQUIRED) ctx.experimental.validate_task_mode(TASK_REQUIRED)
# Capture the meta from the request (if present) # Capture the meta from the request (if present)
if ctx.meta is not None and ctx.meta.model_extra: # pragma: no branch if ctx.meta is not None: # pragma: no branch
received_meta[0] = ctx.meta.model_extra.get("custom_field") received_meta[0] = ctx.meta.get("custom_field")
async def work(task: ServerTaskContext) -> CallToolResult: async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Working...") await task.update_status("Working...")

View file

@ -1,5 +1,4 @@
import pytest import pytest
from pydantic import AnyUrl
from mcp import Client from mcp import Client
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
@ -88,14 +87,14 @@ async def test_resource_template_client_interaction():
assert "resource://users/{user_id}/profile" in templates assert "resource://users/{user_id}/profile" in templates
# Read a resource with valid parameters # Read a resource with valid parameters
result = await session.read_resource(AnyUrl("resource://users/123/posts/456")) result = await session.read_resource("resource://users/123/posts/456")
contents = result.contents[0] contents = result.contents[0]
assert isinstance(contents, TextResourceContents) assert isinstance(contents, TextResourceContents)
assert contents.text == "Post 456 by user 123" assert contents.text == "Post 456 by user 123"
assert contents.mime_type == "text/plain" assert contents.mime_type == "text/plain"
# Read another resource with valid parameters # Read another resource with valid parameters
result = await session.read_resource(AnyUrl("resource://users/789/profile")) result = await session.read_resource("resource://users/789/profile")
contents = result.contents[0] contents = result.contents[0]
assert isinstance(contents, TextResourceContents) assert isinstance(contents, TextResourceContents)
assert contents.text == "Profile for user 789" assert contents.text == "Profile for user 789"
@ -103,7 +102,7 @@ async def test_resource_template_client_interaction():
# Verify invalid resource URIs raise appropriate errors # Verify invalid resource URIs raise appropriate errors
with pytest.raises(Exception): # Specific exception type may vary with pytest.raises(Exception): # Specific exception type may vary
await session.read_resource(AnyUrl("resource://users/123/posts")) # Missing post_id await session.read_resource("resource://users/123/posts") # Missing post_id
with pytest.raises(Exception): # Specific exception type may vary with pytest.raises(Exception): # Specific exception type may vary
await session.read_resource(AnyUrl("resource://users/123/invalid")) # Invalid template await session.read_resource("resource://users/123/invalid") # Invalid template

View file

@ -1,7 +1,6 @@
import base64 import base64
import pytest import pytest
from pydantic import AnyUrl
from mcp import Client, types from mcp import Client, types
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
@ -46,12 +45,12 @@ async def test_fastmcp_resource_mime_type():
assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected" assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected"
# Also verify the content can be read correctly # Also verify the content can be read correctly
string_result = await client.read_resource(AnyUrl("test://image")) string_result = await client.read_resource("test://image")
assert len(string_result.contents) == 1 assert len(string_result.contents) == 1
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch" assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
assert string_result.contents[0].mime_type == "image/png", "String content mime type not preserved" assert string_result.contents[0].mime_type == "image/png", "String content mime type not preserved"
bytes_result = await client.read_resource(AnyUrl("test://image_bytes")) bytes_result = await client.read_resource("test://image_bytes")
assert len(bytes_result.contents) == 1 assert len(bytes_result.contents) == 1
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch" assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved" assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved"
@ -104,12 +103,12 @@ async def test_lowlevel_resource_mime_type():
assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected" assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected"
# Also verify the content can be read correctly # Also verify the content can be read correctly
string_result = await client.read_resource(AnyUrl("test://image")) string_result = await client.read_resource("test://image")
assert len(string_result.contents) == 1 assert len(string_result.contents) == 1
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch" assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
assert string_result.contents[0].mime_type == "image/png", "String content mime type not preserved" assert string_result.contents[0].mime_type == "image/png", "String content mime type not preserved"
bytes_result = await client.read_resource(AnyUrl("test://image_bytes")) bytes_result = await client.read_resource("test://image_bytes")
assert len(bytes_result.contents) == 1 assert len(bytes_result.contents) == 1
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch" assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved" assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved"

View file

@ -5,7 +5,6 @@ with parameters like 'text/html;profile=mcp-app' which are valid per RFC 2045.
""" """
import pytest import pytest
from pydantic import AnyUrl
from mcp import Client from mcp import Client
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
@ -63,6 +62,6 @@ async def test_mime_type_preserved_in_read_resource():
async with Client(mcp) as client: async with Client(mcp) as client:
# Read the resource # Read the resource
result = await client.read_resource(AnyUrl("ui://my-widget")) result = await client.read_resource("ui://my-widget")
assert len(result.contents) == 1 assert len(result.contents) == 1
assert result.contents[0].mime_type == "text/html;profile=mcp-app" assert result.contents[0].mime_type == "text/html;profile=mcp-app"

Some files were not shown because too many files have changed in this diff Show more